Need an ASP.NET MVC long running process with user feedback

29,646

Solution 1

Here's a sample I wrote that you could try:

Controller:

public class HomeController : AsyncController
{
    public ActionResult Index()
    {
        return View();
    }

    public void SomeTaskAsync(int id)
    {
        AsyncManager.OutstandingOperations.Increment();
        Task.Factory.StartNew(taskId =>
        {
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(200);
                HttpContext.Application["task" + taskId] = i;
            }
            var result = "result";
            AsyncManager.OutstandingOperations.Decrement();
            AsyncManager.Parameters["result"] = result;
            return result;
        }, id);
    }

    public ActionResult SomeTaskCompleted(string result)
    {
        return Content(result, "text/plain");
    }

    public ActionResult SomeTaskProgress(int id)
    {
        return Json(new
        {
            Progress = HttpContext.Application["task" + id]
        }, JsonRequestBehavior.AllowGet);
    }
}

Index() View:

<script type="text/javascript">
$(function () {
    var taskId = 543;
    $.get('/home/sometask', { id: taskId }, function (result) {
        window.clearInterval(intervalId);
        $('#result').html(result);
    });

    var intervalId = window.setInterval(function () {
        $.getJSON('/home/sometaskprogress', { id: taskId }, function (json) {
            $('#progress').html(json.Progress + '%');
        });
    }, 5000);
});
</script>

<div id="progress"></div>
<div id="result"></div>

The idea is to start an asynchronous operation that will report the progress using HttpContext.Application meaning that each task must have an unique id. Then on the client side we start the task and then send multiple AJAX requests (every 5s) to update the progress. You may tweak the parameters to adjust to your scenario. Further improvement would be to add exception handling.

Solution 2

4.5 years after this question has been answered, and we have a library that can make this task much easier: SignalR. No need to use shared state (which is bad because it can lead to unexpected results), just use the HubContext class to connect to a Hub that sends messages to the client.

First, we set up a SignalR connection like usual (see e.g. here), except that we don't need any server-side method on our Hub. Then we make an AJAX call to our Endpoint/Controller/whatever and pass the connection ID, which we get as usual:var connectionId = $.connection.hub.id;. On the server side of things, you can start your process on a different thread and retutn 200 OK to the client. The process will know the connectionId so that it can send messages back to the client, like this:

GlobalHost.ConnectionManager.GetHubContext<LogHub>()
                              .Clients.Client(connectionId)
                              .log(message);

Here, log is a client-side method that you want to call from the server, hence it should be defined like you usually do with SignalR:

$.connection.logHub.client.log = function(message){...};

More details in my blog post here

Share:
29,646
Jason
Author by

Jason

Software developer based in York, England. I particularly love writing software applications for the web.

Updated on September 21, 2020

Comments

  • Jason
    Jason over 3 years

    I've been trying to create a controller in my project for delivering what could turn out to be quite complex reports. As a result they can take a relatively long time and a progress bar would certainly help users to know that things are progressing. The report will be kicked off via an AJAX request, with the idea being that periodic JSON requests will get the status and update the progress bar.

    I've been experimenting with the AsyncController as that seems to be a nice way of running long processes without tying up resources, but it doesn't appear to give me any way of checking on the progress (and seems to block further JSON requests and I haven't discovered why yet). After that I've tried resorting to storing progress in a static variable on the controller and reading the status from that - but to be honest that all seems a bit hacky!

    All suggestions gratefully accepted!

  • Jason
    Jason almost 14 years
    Thanks Darin, I tried your code but still get all of the requests 'backing up' after one another. Having spent this morning slowly trawling for an answer I'm now starting to think that this could be related to Session not allowing multiple requests simultaneously, forcing requests to be synchronous. One change I had to make was your use of Task.Factory since I'm currently working in .NET 3, not 4. Do you think Task.Factory would resolve the Session locking problem?
  • Jason
    Jason almost 14 years
    Additionally I have just got your code running and I did it by removing any code in my controller that refers to Session, so that's obviously my problem. Unfortunately my controllers all require various bits of data I'm holding in Session (and retrieved within a BaseController that they all extend) so I can't see any simple way around that problem.
  • Darin Dimitrov
    Darin Dimitrov almost 14 years
    The Session is not always available in asynchronous actions.
  • Jalal El-Shaer
    Jalal El-Shaer over 12 years
    If these actions are calling a service class, should the class expose its progress through an "event" like the old fashioned windows apps ?
  • Darin Dimitrov
    Darin Dimitrov over 12 years
    @jalchr, no, you should have the same system of tasks implemented on your service which allows you to start an operation and then have another service method which will allow you to query the progress of a given task on the service side.
  • Jatin
    Jatin over 10 years
    @DarinDimitrov If the Session is not always available, then how to authenticate user who is making the async ajax request. I have custom membership and role provider, which stores some attributes in session. I seem kind of stuck with this SessionState = disabled stuff. Here is the original question that I posted with the issue that I had been facing (stackoverflow.com/questions/20396154/…)
  • Darin Dimitrov
    Darin Dimitrov over 10 years
    @Nirvan, you could use ASP.NET forms authentication to authenticate the user who is making the request. The fact that you have some custom membership and roles providers that are storing things in the ASP.NET Session is entirely your problem. Personally I would never rely on such a thing.
  • Erik Funkenbusch
    Erik Funkenbusch over 9 years
    Of course there was a comment about SignalR posted almost 2 years ago on the question above. You really shouldn't just post a link to your blog, as that's borderline spammy and link-only answers are frowned upon.
  • Jon Koeter
    Jon Koeter almost 9 years
    I like this solution. Notice that for IE9 (I don't know about other IE versions) you should do '/home/sometaskprogress' + new Date().getTime(), otherwise it's not going to hit your controller after it did once. It will use it's cache.
  • Jess
    Jess almost 7 years
    This seems like a pretty good answer. Is anyone using SignalR instead of polling as in Darrin's answer?
  • David Silva-Barrera
    David Silva-Barrera over 5 years
    I made it work, but it waits for return result; in SomeTaskAsync before it makes all the calls to SomeTaskProgress. I can see the flow only by using with breakpoints, cause in the Browser all changes ocurre so fast that I only reach to see the last values: (100% and "result").
  • David Silva-Barrera
    David Silva-Barrera over 5 years
    Now it's fine. But I noticed that the few first call for progress status are delayed to get it's related response and after a while they all arrive very fast and after that it starts to request and responde at the suppose frame of time. So, for a long time task will work well, I hope.
  • Kiquenet
    Kiquenet over 5 years
    what is AsynManager ?
  • Abdul Nasir Khayam
    Abdul Nasir Khayam about 5 years
    @DarinDimitrov when this SomeTaskCompleted method call. it never call in pipeline. can you elaborate more
  • Gabriel Marques
    Gabriel Marques about 5 years
    Damn, great and clean answer!!
  • jacob mathew
    jacob mathew about 2 years
    I know this is old but this is exactly what i need to do - trying this and I get the HttpContext has an issue 'Reference to type 'HttpContextBase' claims it is defined in 'System.Web', but it could not be found- anybody solve that? - or is there a better way now