MonkRocker
MonkRocker

Reputation: 3

is an async controller apropos here?

I have a system whereby users can upload sometimes large(100-200 MB) files from within an MVC3 application. I would like to not block the UI while the file is uploading, and after some research, it looked like the new AsyncController might let me do what I'm trying to do. Problem is - every example I have seen isn't really doing the same thing, so I seem to be missing one crucial piece. After much futzing and fiddling, here's my current code:

public void CreateAsync(int CompanyId, FormCollection fc)
    {
        UserProfile up = new UserRepository().GetUserProfile(User.Identity.Name);
        int companyId = CompanyId;
        // make sure we got a file..
        if (Request.Files.Count < 1)
        {
            RedirectToAction("Create");
        }

        HttpPostedFileBase hpf = Request.Files[0] as HttpPostedFileBase;
        if (hpf.ContentLength > 0)
        {
            AsyncManager.OutstandingOperations.Increment();

            BackgroundWorker worker = new BackgroundWorker();
            worker.DoWork += (o, e) =>
                {
                    string fileName = hpf.FileName;
                    AsyncManager.Parameters["recipientId"] = up.id;
                    AsyncManager.Parameters["fileName"] = fileName;
                };
            worker.RunWorkerCompleted += (o, e) => { AsyncManager.OutstandingOperations.Decrement(); };
            worker.RunWorkerAsync();

        }

        RedirectToAction("Uploading");
    }

public void CreateCompleted(int recipientId, string fileName)
    {
        SystemMessage msg = new SystemMessage();
        msg.IsRead = false;
        msg.Message = "Your file " + fileName + " has finished uploading.";
        msg.MessageTypeId = 1;
        msg.RecipientId = recipientId;
        msg.SendDate = DateTime.Now;
        SystemMessageRepository.AddMessage(msg);
    }

    public ActionResult Uploading()
    {
        return View();
    }

Now the idea here is to have the user submit the file, call the background process which will do a bunch of things (for testing purposes is just pulling the filename for now), while directing them to the Uploading view which simply says "your file is uploading...carry on and we'll notify you when it's ready". The CreateCompleted method is handling that notification by inserting a message into the users's message queue.

So the problem is, I never get the Uploading view. Instead I get a blank Create view. I can't figure out why. Is it because the CreateCompleted method is getting called which shows the Create view? Why would it do that if it's returning void? I just want it to execute silently in the background, insert a message and stop.

So is this the right approach to take at ALL? my whole reason for doing it is with some network speeds, it can take 30 minutes to upload a file and in its current version, it blocks the entire application until it's complete. I'd rather not use something like a popup window if I can avoid it, since that gets into a bunch of support issues with popup-blocking scripts, etc.

Anyway - I am out of ideas. Suggestions? Help? Alternate methods I might consider?

Thanks in advance.

Upvotes: 0

Views: 1311

Answers (1)

tugberk
tugberk

Reputation: 58494

You are doing it all wrong here. Assume that your action name is Create.

  1. CreateAsync will catch the request and should be a void method and returns nothing. If you have attributes, you should apply them to this method.
  2. CreateCompleted is your method which you should treat as a standard controller action method and you should return your ActionResult inside this method.

Here is a simple example for you:

[HttpPost]
public void CreateAsync(int id) { 

    AsyncManager.OutstandingOperations.Increment();

    var task = Task<double>.Factory.StartNew(() => {

        double foo = 0;

        for(var i = 0;i < 1000; i++) { 
            foo += Math.Sqrt(i);
        }

        return foo;

    }).ContinueWith(t => {
        if (!t.IsFaulted) {

            AsyncManager.Parameters["headers1"] = t.Result;
        }
        else if (t.IsFaulted && t.Exception != null) {

            AsyncManager.Parameters["error"] = t.Exception;
        }

        AsyncManager.OutstandingOperations.Decrement();
    });

}

public ActionResult CreateCompleted(double headers1, Exception error) { 

    if(error != null)
        throw error;

    //Do what you need to do here

    return RedirectToAction("Index");
}

Also keep in mind that this method will still block the till the operation is completed. This is not a "fire and forget" type async operation.

For more info, have a look:

Using an Asynchronous Controller in ASP.NET MVC

Edit

What you want here is something like the below code. Forget about all the AsyncController stuff and this is your create action post method:

    [HttpPost]
    public ActionResult About() {

        Task.Factory.StartNew(() => {

            System.Threading.Thread.Sleep(10000);

            if (!System.IO.Directory.Exists(Server.MapPath("~/FooBar")))
                System.IO.Directory.CreateDirectory(Server.MapPath("~/FooBar"));

            System.IO.File.Create(Server.MapPath("~/FooBar/foo.txt"));

        });

        return RedirectToAction("Index");
    }

Notice that I waited 10 seconds there in order to make it real. After you make the post, you will see the it will return immediately without waiting. Then, open up the root folder of you app and watch. You will notice that a folder and file will be created after 10 seconds.

But (a big one), here, there is no exception handling, a logic how to notify user, etc.

If I were you, I would look at a different approach here or make the user suffer and wait.

Upvotes: 2

Related Questions