3

I'm stuck in a really bizarre situation here. It's complicated to explain but I'll try my best.

Detailed explanation of the issue:

On every top Nav click (Green donuts/circles), or next button, I must submit the form, if it exists and is valid. If not valid, form.valid() triggers validation errors and return false would stop any further propagation. This setup was working flawlessly until I noticed a strange behavior which isn't very persistence. Form on my 3rd tab, specifically, is quite data heavy. When I hit next button it should practically go thru the same process: check for an existing form, if valid, then submit. Submit calls the POST action method and when post completes it GETs the view for next tab. It works like this 5/10 times but at other times GET executes before the POST, which causes next page to load with incomplete data. When I put breakpoints to debug, I see GET for the next tab executing before POST of the current tab.

UI Explained:

I have a UI with 4 navigation <a> buttons on top - in the center there's a always a form - and at the bottom I have Previous & Next buttons.

enter image description here

Forms are constructed in MVC using Ajax.BeginForm

For each Nav link <a> element on top, I have a JavaScript function

var LoadTabs = function (e, arg) {  
    // This is to validate a form if one of the top links is clicked and form has incomplete fields...
    if (arg !== "prev" && arg !== "next") {
        if (!window.ValidateForm(false)) return false;
    }

    var url = $(this).attr('data'); // this contains link to a GET action method
    if (typeof url != "undefined") {
        $.ajax(url, { context: { param: arg } }).done(function (data) {                
            $('#partialViewContainer').html(data);
        });
    }
}

This function above binds to each top link on page load.

$('.navLinks').on('click', LoadTabs);

My Next & Previous buttons basically trigger the click event i.e. LoadTabs function.

$('button').on('click', function () { 
        if (this.id === "btnMoveToNextTab") {
            if (!window.ValidateForm(true)) return false;                

            $.ajax({
                url: url,
                context: { param: 'next' },
                method: "GET",
                data: data,
                success: function(response) {
                    if (typeof response == 'object') {
                        if (response.moveAhead) {
                            MoveNext();
                        }
                    } else {
                        $('#mainView').html(response);
                    }
                    ScrollUp(0);
                }
            });
        } 

        if (this.id === "btnMoveToPreviousTab") {
            MoveBack();
        }
        return false;
    });


MoveNext() Implementation is as below:

function MoveNext() {        
    var listItem = $('#progressbarInd > .active').next('li');        

    listItem.find('.navLink').trigger('click', ['next']);
    ScrollUp(0);
}

The problem is, for some reasons, when Nav Link 3 is active and I hit NEXT button - Instead of posting the form first via form.submit() - the nav 4 gets triggered - hence GET for nav 4 runs before form POST of nav 3.

My ValidateForm method is basically just checking if the form exists and is valid then Submit, else returns false. Its as below:

function ValidateForm(submit) {

    var form = $('form');

    // if form doesn't exist on the page - return true and continue
    if (typeof form[0] === "undefined") return true;

    // now check for any validation errors
    if (submit) {
        if (!$(form).valid()) {                
            return false;
        } else {
            $(form).submit();
        }
    } 
    else {
        return true;
    }

    return true;
}

My speculation is that form.submit does get triggered as it should be but since submit takes a little longer to finish it continues with the next code block in the button onclick event.

I first thought that this is a server side issue as in the POST I'm saving a big chunk of data with a few loops, and any code block that's process heavy I have that part in

var saveTask = Task.Factory.StartNew(() => ControllerHelper.SomeMethod(db, model)); Task.WaitAll(saveTask);

WaitAll will wait and pause the execution until SomeMethod finishes executing. I'm not sure how can I lock a process in JavaScript and wait for it to finish execution. Because I think If i can somehow lock the form.submit() in ValidateForm until its finished processing .. via a callback method perhaps...

Please if anyone can put me in right direction, I'd greatly appreciate the help. If you need more information please let me know I'd be happy to provide!

Mr Lister
  • 45,515
  • 15
  • 108
  • 150
Johny
  • 387
  • 1
  • 7
  • 20
  • Why are you using `Ajax.BeginForm()` is your using `$.ajax()`. But the issue is that ajax is async. When you call `ValidateForm()` and its valid, an ajax call is being made to the POST method. The code then steps into the the next `$.ajax()` call to your GET method. They are happening at the same time and there is no guarantee which is going to be completed first. –  Mar 25 '17 at 01:43
  • We didn't have these validation logics before it was pretty simple ajax forms that will submit and update the page with response (partial view returned from an action method). Thanks for pointing it out though. and Yes i thought so too... I'm just trying to understand if there's a way to stop/pause the code from running until POST is complete... – Johny Mar 25 '17 at 01:47
  • Its not really clear what the the 'Next' and 'Previous' and 'NavLink' buttons are for. If this is for some kind of 'wizard' when you must complete each step in order, then your forms 1-3 should just have a 'Next' submit button, and form 4 a 'Complete' button (and the 'bar' at the top should just be a visual indicator). In any case, you should be handling the forms `.submit()` event, checking `isvalid()` (and cancelling if so) and then making a $.post()` and that method should return the appropriate partial view to update the DOM. –  Mar 25 '17 at 01:53
  • And if the forms can be completed in any order, surely the user should just be able to jump to which ever form they want even if they have started to complete the previous form (perhaps you just display a confirm before allowing them to go to another form) –  Mar 25 '17 at 01:56
  • This is how application UI structure has to be unfortunately...I cannot change that. Its really not just 4 tabs on top. There are 4 master tabs on top (lol) which have 4 sub tabs (NavLinks - circles/donuts) each. Forms don't exist on every page. Next/Previous are part of the main view and where the form is in that photo I attached, thats a partial view container where the views are dynamically loaded... And no forms cannot be in completed in any order - well at least until each of them have been filled, then user can jump around the app. But 1st time - its a strict order they must follow, – Johny Mar 25 '17 at 02:02
  • Then get rid of `Ajax.BeginForm()`. Handle each forms `.submit()` event and if its valid, make a `$.post()` and in its success callback, update the DOM with the new form/view that the POST method returns (and re-parse the validator so you get client side validation for the next form) –  Mar 25 '17 at 02:14
  • If you read my question thats exactly what I do. I read a form - validate it - Submit it - get the next view. I doubt getting rid of ajax.beginform() would help, I agree to what you're saying but I don't think this will solve the issue. :( – Johny Mar 25 '17 at 02:15
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/138996/discussion-between-stephen-muecke-and-johny). –  Mar 25 '17 at 02:21
  • I can definitely try the success callback but I'd have to rethink the dynamic nature of the application. If there's no other solution... i probably would end up doing that... – Johny Mar 25 '17 at 02:22
  • @Chris MoveNext() doesn't actually get or post anything - it basically marks the next navlink as active. Yes it is called on success, but of "GET" (of next view) which follows "POST" of current view. Whats happening here. POST runs after the GET. Ugh i find it really hard to explain this bizarre behavior sorry :( – Johny Mar 25 '17 at 03:02
  • Maybe have a look at this: http://stackoverflow.com/questions/11534690/how-to-do-a-jquery-callback-after-form-submit – Chris Mar 25 '17 at 03:06
  • The alternative would be to do the submit yourself, so you would add an anonymous function inside the submit() then just send the stuff manually, but you already went through all the trouble to create this complex solution, it would be a waste of time to do that now. Maybe try the link I gave above and see if you can use something there – Chris Mar 25 '17 at 03:07
  • @Chris Thanks. I'll take a look! – Johny Mar 25 '17 at 03:08

1 Answers1

7

Ajax is async, and your forms submit which is using Ajax.BeginForm() is using ajax. What is happening is that when you click your 'Next' button, which triggers the $('button').on('click', function () { code:

  1. You call the ValidateForm() function (and assuming its valid), your $(form).submit(); line of code starts making a ajax POST
  2. The code progresses to the final return true; line while the ajax call is executing.
  3. Because the ValidateForm() function returned true, the $.ajax GET call now starts, but at that point the ajax POST in the ValidateForm() function may not have finished executing causing your GET method to return invalid data

You need to change your code so that the GET call is made once the POST method call has completed. And since your using the $.ajax() methods throughout your code, and $.ajax() gives you more flexibility, it seems unnecessary to use Ajax.BeginForm() (and the extra overhead of including the jquery.unbtrusive-ajax.js script). You should also be handling the forms .submit() function (if you do not want the 'Next' button to be a submit button in the form, you could just trigger the .submit() event in the buttons .click() handler)

$(document).on('submit', 'form', function(e) {
    e.preventDefault(); // cancel default submit
    var form = $(this);
    if (!form.valid()) {
        return; // will display the validation errors
    }
    .... // get the relevant urls to the GET and POST methods etc
    $.post(postUrl, form.serialize(), function(data) {
        .... // not clear if your [HttpPost] method returns anything
    }).done(function() {
        $.get(getUrl, someData, function(response) {
            .... // Update the DOM with the next form?
            .... // Re-parse the validator for client side validation
        }
    }).fail(function() {
        .... // code that you might run if the code in the [HttpPost] method fails
    });
});

You should also consider returning the appropriate 'next' view in the [HttpPost] method so that you don't then needs to make a second call back to the server to get it.

It is also worth reading the Deferred Object documentation and the use of $.when(), $.then() etc.

  • I ended up using deferred object. if form exists and is valid, return `$.post()`, else return deferred.resolve.promise(). I make the $.get call in `$.then()`'s callback function. I think this has resolved my problem. Thanks for taking your time to help me out. – Johny Mar 27 '17 at 15:03