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.
You are doing it all wrong here. Assume that your action name is Create.
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.
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.
Related
I am working on an ASP.NET MVC web service. In a web page, when a user clicks on a button, this triggers a complex method that takes a bit of time to finish. I want to redirect the user to a waiting page and then, when the process is finished, to redirect the user to a new page.
When the process is done it raises an event, which I can listen to from the controller. But I cannot make the last step to work (the controller redirecting to the new page upon receiving the event).
Here is my very naïve attempt at doing it (with simpler names):
public MyController()
{
EventsControllerClass.ProcessComplete += new EventHandler<MyArgsClass>(OnEventReceived);
}
private void OnEventReceived(object sender, MyArgsClass eventArguments)
{
RedirectToPage();
}
private ActionResult RedirectToPage()
{
return RedirectToAction("PageName");
}
After many days working on this, I have a viable solution. It may not be pretty, but it works, and maybe some ideas can be useful for other people, so here it goes:
I will explain the solution to my particular problem: I need a button to redirect to a "waiting" page while a longer process runs in the background and raises an event when it is finished. When this event is received, we want to redirect the user (automatically) to a final page.
First, I created a class to listen to the event. I tried doing this directly in the controller, but you need to be careful about signing and unsigning, because apparently controllers get created and destroyed at each request. In this "listener class" I have a bool property that is set to "true" when the event is received.
When the first action is triggered, the controller normally redirects to the "wait" page, where I have this simple java script redirecting to the new action:
<script type="text/javascript">
window.location = "#Url.Action("WaitThenRedirect", "AuxiliaryControllerName")";
</script>
This sets in motion the long process (through another event). The key is that I do this with an asynchronous action (this controller inherits from AsyncController). (Note I used an auxiliary controller. This is to keep all asynchronous stuff apart.) This is how this looks (more info here):
public static event EventHandler<AuxiliaryEventsArgs> ProcessReady;
public void WaitThenRedirectAsync()
{
AsyncManager.OutstandingOperations.Increment();
ProcessReady += (sender, e) =>
{
AsyncManager.Parameters["success"] = e.success;
AsyncManager.OutstandingOperations.Decrement();
};
WaitForEvent();
}
public ActionResult WaitThenRedirectCompleted(bool success)
{
if (success)
{
return RedirectToAction("RedirectToView", "ControllerName");
}
else
{
return RedirectToAction("UnexpectedError", "ControllerName");
}
}
private void WaitForEvent()
{
bool isWaitSuccessful = true;
int waitingLoops = 0;
int waitingThreshold = 200;
int sleepPeriod = 100; // (milliseconds)
while (!EventsListener.IsTheThingReady())
{
System.Threading.Thread.Sleep(sleepPeriod);
++waitingLoops;
if (waitingLoops > waitingThreshold)
{
System.Diagnostics.Debug.WriteLine("Waiting timed out!");
isWaitSuccessful = false;
break;
}
}
isWaitSuccessful = true;
if (null != ProcessReady)
{
AuxiliaryEventsArgs arguments = new AuxiliaryEventsArgs();
arguments.success = isWaitSuccessful;
try
{
ProcessReady(null, arguments);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Error in event ProcessReady" + ex);
}
}
}
I believe it is possible to use ajax syntax for alternative solutions, but this is what I have and it works nicely. I believe this is not a very common need, but hopefully someone will benefit!
public async Task<Utils.TReportReturn> GetReportDataColumnSettings(AdminTechnicianReportInput input)
{
input.CustomerId = m_customerId;
input.UserId = m_userId;
input.TimeOffset = m_timeOffset;
TReportReturn objTReportReturn = new TReportReturn();
objTReportReturn.ReportColumns = await new ReportsEntityContext().GetTechniciansReportColumns<AdminTechnicianReportInput>(input);
objTReportReturn.ReportSettings = await new ReportsEntityContext().GetGeneralSettingsData<AdminTechnicianReportInput>(input);
objTReportReturn.ReportData = await new ReportsDbContext().GetUserListDynamic<AdminTechnicianReportInput>(input);
return objTReportReturn;
}
public class HomeController : Controller
{
public ActionResult Index()
{
AdminTechnicianReportInput objInput = new AdminTechnicianReportInput();
objInput.CustomerId = 1;
objInput.UserId = 5477;
objInput.AddUpdateUserid = 0;
objInput.intRowsPerPage = 200;
objInput.intPageIndex = 0;
objInput.intTarget = 0;
objInput.intStatusId = 0;
objInput.strSearchQuery = "";
objInput.strSortColumn = "Id";
objInput.strSortDirection = "ASC";
var result = new ReportsModel(1, 5477, 0).Administration.AdminTechnicianReport.GetReportDataColumnSettings(objInput).Result;
ViewBag.Title = result.ReportSettings.CustomerName + "__" + result.ReportSettings.PrintedOn;
return View();
}
}
The GetReportDataColumnSettings function calls many async methods which is the the model library dll.
Index() method in HomeController is an mvc method and I want to get the return in the result variable. Itried getawaiter().getmethod(), but it didn't worked. I do not prefer to change the Index() method as asynk Task. Please suggest another way to get the result
I do not prefer to change the Index() method as asynk Task.
Why not? That is by far the easiest and most correct solution.
The second easiest is to just make everything synchronous. That is, if you won't go async all the way, then go sync all the way.
There are a variety of hacks for calling async from sync code that may or may not work in your situation; none of these hacks work everywhere. For more details, see my async brownfield article.
In your case, your initial attempt was to use the Blocking Hack. For this to work appropriately on the pre-Core ASP.NET, you'd have to use ConfigureAwait(false) for every await in that entire method call tree. This is usually possible (not always possible, of course, because library code may or may not use it), but it causes maintenance problems (forget just one, and you've got a race condition that may end in deadlock). I explain why the deadlock happens in detail on my blog.
If you won't go async all the way, and if you can't go sync all the way, then I'd recommend the Boolean Sync Argument Hack, which requires the entire call stack to support both synchronous and asynchronous callers. It would look something like this for the code you posted:
private async Task<Utils.TReportReturn> DoGetReportDataColumnSettingsAsync(AdminTechnicianReportInput input, bool sync)
{
input.CustomerId = m_customerId;
input.UserId = m_userId;
input.TimeOffset = m_timeOffset;
TReportReturn objTReportReturn = new TReportReturn();
objTReportReturn.ReportColumns = await new ReportsEntityContext().GetTechniciansReportColumns<AdminTechnicianReportInput>(input, sync);
objTReportReturn.ReportSettings = await new ReportsEntityContext().GetGeneralSettingsData<AdminTechnicianReportInput>(input, sync);
objTReportReturn.ReportData = await new ReportsDbContext().GetUserListDynamic<AdminTechnicianReportInput>(input, sync);
return objTReportReturn;
}
private Task<Utils.TReportReturn> GetReportDataColumnSettingsAsync(AdminTechnicianReportInput input)
{ return DoGetReportDataColumnSettingsAsync(input, sync: false); }
private Utils.TReportReturn GetReportDataColumnSettings(AdminTechnicianReportInput input)
{ return DoGetReportDataColumnSettingsAsync(input, sync: true).GetAwaiter().GetResult(); }
The key with this pattern is that any method taking a sync parameter must return a completed task when that argument is set to true.
I am using Edge.js so that I can call Node.js from C#. According to the documentation in the link I would do that similar to the following:
[HttpPost]
public ActionResult Input(InputModel obj)
{
validateInput(obj);
return View();
}
private async void validateInput(object obj)
{
var func = Edge.Func(#"
return function (data, callback){
var username = data.username,
email = data.email;
callback(null, username);
}
");
ViewBag.Msg = (string)await func(obj);
}
However, I get the following run time error:
Additional information: An asynchronous operation cannot be started at this time.
Asynchronous operations may only be started within an asynchronous handler or module or during
certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the
Page is marked <%# Page Async="true" %>. This exception may also indicate an attempt to call an
"async void" method, which is generally unsupported within ASP.NET request processing. Instead,
the asynchronous method should return a Task, and the caller should await it.
My question is two-fold:
1.How do I make the page, async=true. I know how to do this for a web forms project but not a MVC project.
2.Is there a better way to do what I am trying to do? A red flag will probably go up when you see that I am returning void however this is do to the fact that Edge.js is being used. Even so, I have tried returning a Task and then task.Wait() in the calling method but the task never finishes.
After trying some different things, the following solution worked for me.
Even though I answered my own question, and it seems trivial, I am not removing this question as there are not a lot of knowledge on the web about Edge.js.
[HttpPost]
public async Task<ActionResult> Input(InputModel obj)
{
ViewBag.Msg = await validateInput(obj);
return View();
}
private async Task<string> validateInput(object obj)
{
var func = Edge.Func(#"
return function (data, callback){
var username = data.username,
email = data.email;
callback(null, username);
}
");
return (string)await func(obj);
}
I am using a DevExpress MVC Pivot Grid and trying to work out some problems with the loading and saving of layouts. So far I have the following:
I have set my CustomActionRouteValues in the PivotGridSettings as follows:
CustomActionRouteValues = new { Controller = "Home", Action = "PivotGridCustomCallback" },
Which points to the following:
public ActionResult PivotGridCustomCallback(string action, string reportName)
{
if (string.IsNullOrEmpty(reportName))
{
reportName = "Report 1";
}
var settings = PivotGridLayoutHelper.DefaultPivotGridSettings;
if (action == "Save")
{
// TODO: Find a better solution than this. At the moment, if Save is called once, it is then called again every time the user changes the layout.. which is why we have the 'saved' variable here.
bool saved = false;
settings.AfterPerformCallback = (sender, e) =>
{
if (saved)
{
return;
}
SaveLayout(((MVCxPivotGrid)sender).SaveLayoutToString(), reportName);
saved = true;
};
}
else if (action == "Load")
{
// TODO: Find a better solution than this. At the moment, if Load is called once, it is then called again every time the user changes the layout.. which is why we have the 'loaded' variable here.
bool loaded = false;
string layoutString = LoadLayout(reportName);
if (!string.IsNullOrEmpty(layoutString))
{
settings.BeforeGetCallbackResult = (sender, e) =>
{
if (loaded)
{
return;
}
((MVCxPivotGrid)sender).LoadLayoutFromString(layoutString, PivotGridWebOptionsLayout.DefaultLayout);
loaded = true;
};
}
}
ViewBag.PivotSettings = settings;
return PartialView("PivotPartial");
}
The problem, as you can see in the code comments, is that after performing an action just one time, it then gets called EVERY time I make any sort of change. So, for example... say I load a report.. that's fine.. but then when I try expand something or add a field.. or do ANYTHING, nothing seems to happen on the UI.. and I figured out that's because immediately, this code gets called again:
settings.BeforeGetCallbackResult = (sender, e) =>
{
((MVCxPivotGrid)sender).LoadLayoutFromString(layoutString, PivotGridWebOptionsLayout.DefaultLayout);
};
That just keeps resetting the values to the saved layout, which means the UI looks like it's unresponsive when trying to change anything.
This is why I now have the boolean variable called loaded to check if it's already loaded. That works.. but it's an ugly hack.. because it's making unnecessary trips to the server each and every time the user does anything on the pivot grid.
Surely there must be a way to prevent these actions from firing all the time?
Need to compare the original value with the current value in order to do some updates on objects prior to save changes. How to get the original value of an object? The following does not work:
public ActionResult Edit(int id = 0)
{
Project project = db.Projects.Find(id);
...
db.Projects.Attach(project); // ATTACH here
...
}
[HttpPost]
public ActionResult Edit(Project project)
{
if (ModelState.IsValid)
{
db.Entry(project).State = EntityState.Modified; // The STATE was DETACHED here...
...
}
Is this a configuration issue, or did I miss something? Thanks in advance for any help!
The problem with your solution is the lifetime of the controller. By default a new instance of the controller is generated for each client request. This means that you need to load the original value inside the POST method of edit, because you cannot share the instance of your context.
I have edited the code sample after #Gerard's comment
[HttpPost]
public ActionResult Edit(Project project)
{
if (ModelState.IsValid)
{
var original_value = db.Projects.Find(project.ProjectId);
or
var original_value_detached = db.Projects.AsNoTracking().Where(P => P.ProjectId == project.ProjectId).FirstOrDefault();
}
}
Or you write your own implementation of the controller factory to share you context between two requests, but you should consider requests from different clients. You can find an example here
It looks like you are implementing concurrency. May be, you can try to catch your changes using DbUpdateConcurrencyException. Something like this:
try
{
...
db.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var dbValue = (Project)entry.GetDatabaseValues().ToObject();
if(dbValue.State == EntityState.Modified)
{
//***your code
}
}