-1

I have below ajax code and it is working ok:

       $.ajax({
           //async: false,
           url: "/Tests/GetData/",
           type: 'POST',
           dataType: 'json',
           contentType: "application/json; charset=utf-8",
           success: function (data) {
               $.each(data, function (i, item) {
                   $.ajax({
                       //async: false,
                       url: "/Tests/DoTask/",
                       type: 'POST',
                       data: { taskName: item.TaskName },
                       success: function () {
                           $("#Status").append('Task PASSED.<br/>');
                       },
                       error: function () {
                           $("#Status").append('Task FAILED!<br/>');
                       },
                       beforeSend: function () {
                           $("#Status").append('Doing task...<br/>');
                       }
                   });

               });
               $("#Status").append('Process completed.</span><br/>');
           },
           error: function (XMLHttpRequest, textStatus, errorThrown) {
               $("#Status").append('Error: ' + errorThrown + '<br/>');
           },
           beforeSend: function () {
               $("#Status").append('<br/>Process started.<br/>');
           }
       });

The problem with this is that it is asynchronous so the messages in the view control are being displayed unordered, so I decided to put within ajax the option async:false but this cause web application get completely blocked althought the messages are displayed ordered...bad idea so I thought in implementing a queue to serve the ajax calls and not use the option async:false. I googled and I found this one:

How can jQuery deferred be used?

Below the code from the above page:

/* Class: Buffer
 *  methods: append
 *
 *  Constructor: takes a function which will be the task handler to be called
 *
 *  .append appends a task to the buffer. Buffer will only call a task when the 
 *  previous task has finished
 */
var Buffer = function(handler) {
    var tasks = [];
    // empty resolved deferred object
    var deferred = $.when();

    // handle the next object
    function handleNextTask() {
        // if the current deferred task has resolved and there are more tasks
        if (deferred.isResolved() && tasks.length > 0) {
            // grab a task
            var task = tasks.shift();
            // set the deferred to be deferred returned from the handler
            deferred = handler(task);
            // if its not a deferred object then set it to be an empty deferred object
            if (!(deferred && deferred.promise)) {
                deferred = $.when();
            }
            // if we have tasks left then handle the next one when the current one 
            // is done.
            if (tasks.length > 0) {
                deferred.done(handleNextTask);
            }
        }
    }

    // appends a task.
    this.append = function(task) {
        // add to the array
        tasks.push(task);
        // handle the next task
        handleNextTask();
    };
};

It seems promising so I have decided to try it, so I have modified my ajax code and below the result, note that i have replace the inner ajax block:

       $.ajax({
           //async: false,
           url: "/Tests/GetData/",
           type: 'POST',
           dataType: 'json',
           contentType: "application/json; charset=utf-8",
           success: function (data) {
               $.each(data, function (i, item) {
                   Buffer({
                       //async: false,
                       url: "/Tests/DoTask/",
                       type: 'POST',
                       data: { taskName: item.TaskName },
                       success: function () {
                           $("#Status").append('Task done.<br/>');
                       },
                       error: function () {
                           $("#Status").append('Task failed!<br/>');
                       },
                       beforeSend: function () {
                           $("#Status").append('Doing task...<br/>');
                       }
                   });

               });
               $("#Status").append('Process completed.</span><br/>');
           },
           error: function (XMLHttpRequest, textStatus, errorThrown) {
               $("#Status").append('Error: ' + errorThrown + '<br/>');
           },
           beforeSend: function () {
               $("#Status").append('<br/>Process started.<br/>');
           }
       });

I am not sure If I am using correctly the call to the buffer, sure not because i have put a breakpoint in my action DoTask in the controller and never stops so I am not queuing correctly each task, call to Buffer seems incorrect.... So what am i doing wrong?

First attempt (Solution from Paul Grime): I have done your solution, but I am trying to modify some things that i am not able to do:

1) My DoTask returns an http code 200 (if task done ok) or 500 (if task not done ok) HttpStatusCodeResult(HttpStatusCode.OK)/HttpStatusCodeResult(HttpStatusCode.NotFound) so in the string displayed (that string that begins with done ...) I want to add the result of the doTask, for example:

if doTask has done the task ok:

"Result" : "Passed" => done { "Result" : "Passed", ...}

If doTask hasn't finished the task correctly:

"Result" : "Failed" => done { "Result" : "Failed", ...}

2) I have grouped my tasks, so first I launch to do a type of tasks, then when those has finished, independenty if they go ok or not, I need to launch the next type of tasks to be done, and so on... How to modify your code to do it?

Second attempt:

Controller:

[HttpPost]
public JsonResult GetData()
{
    var data = (dynamic)null;
    using (BBDDContext context = new BBDDContext())
    {
        data = context.MyObject.Where(o => o.TypeId == 1).OrderBy(k => k.Name).Select(obj => new
        {
            name =obj.Name,
            description =obj.Description
        }).ToList();          
    }

    return Json(data, JsonRequestBehavior.AllowGet);
}

View:

function getTasks() {
    return ajax({
        url: "/Tests/GetData/",
        type: 'POST',
        dataType: 'json',
        contentType: "application/json; charset=utf-8"
    }).then(function (data) {
        // data contains a list of pairs [Name IP]            
        return ok(createObject("status", "ok", "op", "getTasks", "data", JSON.stringify(data)));
    }, function () {
        return ok(createObject("status", "fail", "op", "getTasks"));
    });
}

for some reason when trying to print "Received GetData results..." the first part ("status", "ok", "op", "getTasks") is missed, only last one, that related to "data" is printed (displayed).

Community
  • 1
  • 1
Willy
  • 9,848
  • 22
  • 141
  • 284

1 Answers1

1

This jsfiddle (jQuery 1.10.1) and this jsfiddle (jQuery 1.7.2) might help you get started (difference is 1.10.1 version uses Deferred.then() and 1.7.2 version uses Deferred.pipe().

What I try to look for when I'm using deferreds/promises is that I've reduced the callback hell, or more simply, reduced the level of nesting introduced as a consequence of nested async callbacks.

First, start with identifying logical functionality, and refactoring that out into well-named functions, each returning deferreds.

function getTasks() {
    return ajax({
        // replace original URL with jsfiddle URL and test data
        //url: "/Tests/GetData/",
        url: "/echo/json/",
        data: jsFiddleData(fakeTasks),

        type: 'POST',
        dataType: 'json',
        contentType: "application/json; charset=utf-8"
    }).then(function(data) {
        return ok(createObject("status", "ok", "op", "getTasks", "data", data));
    }, function() {
        return ok(createObject("status", "fail", "op", "getTasks"));
    });
}

function doTask(task) {
    return ajax({
        // replace original URL with jsfiddle URL and test data
        //url: "/Tests/DoTask/",
        //data: {
        //    taskName: task.TaskName
        //},

        //  + "?" + task.TaskName for cache-busting
        url: "/echo/json/" + "?" + task.TaskName,
        data: jsFiddleData({
            "status": "doing " + task.TaskName
        }),

        type: 'POST',
        dataType: 'json'
    }).then(function(data) {
        return ok(createObject("status", "ok", "op", "doTask", "task", task, "data", data));
    }, function() {
        return ok(createObject("status", "fail", "op", "doTask", "task", task));
    });
}

function doTasks(tasks) {
    // Create a deferred for each task by calling doTask().
    var deferreds = $(tasks).map(function (i, task) {
        postStatus("Sending DoTask request: " + i + "," + JSON.stringify(task));
        return doTask(task);
    }).toArray();

    // return a composite deferred which will
    // wait for each of the doTask requests.
    return $.when.apply($, deferreds);
}

And your app code ends up looking something like:

getTasks().then(function (tasks) {
    postStatus("Received GetData results");
    return doTasks(tasks);
}).then(function (results) {
    postStatus("Received DoTask results");
    for (var i = 0; i < results.length; i++) {
        postStatus('done ' + JSON.stringify(results[i]));
    }
}).fail(function (err) {
    postStatus("Error: " + JSON.stringify(err));
});

In fact, without some of the console.logs, and with some function refactoring, it can read even better:

function showResults(results) {
    postStatus("Received DoTask results");
    for (var i = 0; i < results.length; i++) {
        postStatus('done ' + JSON.stringify(results[i]));
    }
}

getTasks().then(doTasks).then(showResults).fail(function (err) {
    postStatus("Error: " + JSON.stringify(err));
});
Paul Grime
  • 14,970
  • 4
  • 36
  • 58
  • Great! i am trying to do some things in your solution but I am not able to do (i am newbie...,sorry). Could you help me a little bit more? Please, see my updated post to see what i am trying to do. – Willy Sep 22 '13 at 14:54
  • See my updated post please, in first attempt I describe what i am trying to do.Many thanks! – Willy Sep 22 '13 at 15:14
  • http code 400 should not be used for any success - http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error. Should that be 200? – Paul Grime Sep 22 '13 at 15:51
  • sorry, it was a mistake, for success 200. you are right. Do you know how to add the result of each task done? to add it to the string "done..." and how can I do another group of tasks when all those tasks have finished, by concatenating and .always each time until all the tasks groups has finished? could you help me a little more? i am trying to do what I have posted in the new update.many thanks! – Willy Sep 22 '13 at 15:56
  • Hi again, could you tell me how can i add the result of the task done (passed or failed) to the output string "done { ... }". I am trying to do it...if you can tell me or guide me in the right direction I highly appreciate you. – Willy Sep 22 '13 at 18:29
  • If you mean the code `postStatus('done '`, then that only executes when all `doTask` calls are successful. – Paul Grime Sep 22 '13 at 18:47
  • Yes, but I mean, for example, after each task has been done, in my case, it return http code 200 (ok), or http code 400 (bad), this is what returns my DoTask (action in the controller). my question is, how to return this result (passed or failed) in order to get printed when done postStatus('done .....') for each task? – Willy Sep 22 '13 at 19:16
  • please, also see my original post, as you can see for each task done, i print a string that says "Task PASSED" (when entering in success event) or "Task FAILED!" (when entering in error event), i do this for each task so i want to do the same with your code, i mean, for each task, in the line postStatus('done ...' i want to display it (append it) for each task: 'done {"PASSED",...}' or 'done {"FAILED",...}' – Willy Sep 22 '13 at 19:32
  • Ok, I've updated the fiddle in the answer with a new link. See if that matches what you want and if it does I can update the answer. – Paul Grime Sep 22 '13 at 19:43
  • Yes, this is what I want. Now i am trying to adapt your code to my case (model) but for some reason when doTasks is reached, the parameter tasks is undefined in my case... Do you know why? – Willy Sep 22 '13 at 22:32
  • I do not understand why but after calling GetTasks, when 'then' is processed and when printing the response by doing postResponse(response, "Received GetData results" + JSON.stringify(response)); I only get the data part and first part ("status", "ok", "op", "getTasks") is missed. THe only difference respect your code is the getTasks function, my action in the controller returns a list of let's say {name,description} elements that then i return as a json. – Willy Sep 23 '13 at 08:43
  • It doesn't sound like you are transforming the data like the example does. The example uses code like `return ok(createObject("status", "ok", "op", "doTask", "task", task, "data", data));` to return a **new** object that is composed of status info and the actual data returned by the request. – Paul Grime Sep 23 '13 at 09:26
  • I have updated the post (see second attempt) I have put the controller and the view. – Willy Sep 23 '13 at 09:55
  • Also, could you explain what does newPromise, i do not understand the logic inside, sorry I am new on it...thanks. – Willy Sep 23 '13 at 10:37
  • Finally I have discovered why your code is working and mine not. It is because in jsfiddle you are using jQuery 1.10.1 and i am using jQuery 1.7.1 so the output results displayed are completely different from one version to another. You can check in jsfiddle with jQuery 1.7.2, this produces the same as jQuery 1.7.1 but both different from 1.10.1. What I have to change in your code in order to work in jQuery 1.7.1? – Willy Sep 23 '13 at 15:03
  • 1
    1.7.2 and 1.10.1 versions added. Difference is API change - 1.8 started to use `Deferred.then` rather than `Deferred.pipe`. – Paul Grime Sep 23 '13 at 19:03
  • Great!it works like a charm. A lot of thanks!!!!!!Anyway I have installed jQuery 1.10.2 and it is working as well. I would like to know if some things (although this is not maybe in the scope of this thread) can be done with your code as you are familiar with my case:1)Is it possible in this case that message for tasks appear joined?for example, sending...task1... then done ... task1, Sending...task2... then done ... task 2,... and so on. 2) see next comment. – Willy Sep 24 '13 at 09:37
  • 2)I have grouped my tasks by type,2 types of tasks: LowPriority and HighPriority.High Priority tasks are done using the code you provided and then,once high priority done,low priority tasks are done so how to do it?I have done below: $.when(initiate).when(highPriorityTasks).when(lowPriorityTasks).then(terminate) Note initiate is a function that disables button on process start and displays msg:"Process started",and terminate, enables button again once process finished and displays msg "Process finished.",but it is not working.Or better queue all in a defferred queue and then serve? – Willy Sep 24 '13 at 09:44
  • I forgot to say in point 2) that low and high priority tasks have different GetData method as they access to different entities in the database from the controller so, I pass to getTasks method the correct url in each case pointing to the correct action in the controller in charge to extract the data. Also I reused all the code getTask DoTask and DoTasks, the same code written 1 time and used by all the tasks types (highpriotiry and lowpriority and others if any new in the furture). – Willy Sep 24 '13 at 10:12
  • I have done point 2) that i have commented you in the previous comments but i have some issues, see this new thread: http://stackoverflow.com/questions/18982225/unwinding-promises-and-their-handlers – Willy Sep 24 '13 at 14:17
  • Could you tell me how can I do in order to repeat all the process a number of times? I have implemented a version in the link commented you before but it is not working at all. – Willy Sep 24 '13 at 22:21