views:

1092

answers:

6

The ASP.NET MVC pattern of submitting forms via post, then redirecting to the same or different URL is very easy to code.

Imagine this scenario:

  1. User goes to /products/42/edit to view and edit product 42.
  2. They see something crazy on that page, edit it, and hit save. This causes a POST to /products/42/edit
  3. The action updates the data and redirects to or returns the view for /products/42/edit
  4. The user sees the updated data and is happy.
  5. One hour later they click refresh to see if anyone else has messed with product #42.
  6. Because the last retrieval for /products/42/edit was a POST, their browser asks to resubmit the form data. This is annoying and dangerous because it can overwrite someone else's data.

I fear that even if I use two different URLs for the POSTs and GETs (say /products/42/edit and /products/42), that the browser will still ask for the repost and can destroy data. Am I mistaken?

What alternative methods can be used so that after submitting product changes, the user can safely hit refresh to get an updated view?

Update I see now that my question and my design were muddled, my apologies for that. I see that it was a bad idea for me to share URLs (actions) between the POST and the GET. Am I right to assume then that if those two are different, then I won't have the "refresh causes rePOST" problem?

A: 

Ajax form submits?

See MS MVC form AJAXifying techniques and ASP.NET MVC Tip #5 - Submitting an AJAX Form with jQuery,

cletus
AJAXifying the form won't help when the user hits F5.
Darin Dimitrov
Well that depends on if your form is clever enough to figure out the current state doesn't it?
cletus
But if the form was clever enough to figure out the current state it would work without AJAX as well. I think some sample code from Frank Krueger would make things more clear.
Darin Dimitrov
A: 

You are saying that when the user hits refresh you get an outdated view. From this I assume your GET action is using some object stored inside TempData from the POST action. In this case a possible workaround would be to use Session instead of TempData for storing the result of the POST action.

Darin Dimitrov
A: 

In order for your posting form to work in the manner that you describe, it needs to read the data from the database and prepopulate the fields on the form. It needs to do this from the very first time you enter the URL in question.

Then, when you post, it needs to save the values to the database once they are validated. This will complete your round trip loop.

If the page is a blank "new record" page, you are right, it will only round-trip once on a failed validation. On an F5 it will just give you a new blank form, by design.

Robert Harvey
+5  A: 

To your update: yes.

Use /product/{id}/ for viewing, and /product/{id}/edit for editing, and after the edit, redirect them to /product/{id}/.

Problem solved. Was wondering why you are/were using /product/{id}/edit for both viewing and editing.

Femaref
Addendum: if you really want to view and edit in the same page then you should consider ajaxify your form.
Spoike
+1  A: 

If you really want to have the same page for view and edit AND you need to handle concurrency you can includ hidden field that keeps last updated date. So the flow will be:

  1. User1 submits data.
  2. The same page gets rendered BUT with date element (lets say date1).
  3. User2 submits this page.
  4. User1 submits again. The action compares the date1 and actual updated date.
  5. In out case they are different so the action should not update and tell user about it.

This is just another option.

Dmytrii Nagirniak
+4  A: 

Although I agree with what's said in the leading answer about not returning the user to the Edit view again after the post is complete; it doesn't answer the problem of why pushing refresh reposts the form.

Here is the solution to the problem of 'refresh in the browser resulting in another post'.

At the moment you are doing this:

[AcceptVerbs(HttpVerbs.Post)]    // <--  this action will be used for POSTs
EditProduct(string data1, string data2) 
{
    // Handle Data, save to DB
    // Do some work
    return View("EditProduct");  //  <-- You are rendering the view from
}                                //      A post action - this is bad!


When you should in fact do this:


[AcceptVerbs(HttpVerbs.Post)]    //  <-- This action will be used for POSTs
EditProduct(string data1, string data2) 
{
    // Handle Data, save to DB
    // Do some work
    return RedirectToAction("EditProduct"); // <-- Redirect to a GET Action
}

[AcceptVerbs(HttpVerbs.Get)]   //  <-- This action will be used for GETs
EditProduct() 
{

    return View("EditProduct"); // <-- Render the view from the GET action
}                               //     So when you refresh it will refresh the GET

The key point is, don't return a View in response to a POST, otherwise the last Request in the browser was a POST request and pushing refresh in the browser will re-post. Rather, when you have finished your post action, redirect to the "GET" Action using RedirectToAction(). The GET action in turn returns the View. This means that the previous request in the browser was a GET request and if you push refresh it will GET it again, and not repost it. I made the same error when I started with MVC.

reach4thelasers