eWorld.UI - Matt Hawley

Ramblings of Matt

ASP.NET MVC - Using Post, Redirect, Get Pattern

May 8, 2008 23:51 by matthaw

Update: I've updated the sample to MVC Preview 3 bits. While this post is a great "overview" post, you should definately check out the latest sample as it's more streamlined.

imageThe ASP.NET MVC pattern tends to lead itself into a more simplified and "true" HTTP experience by re-introducing  patterns that have been lost, or at least, not followed in many years. One such pattern is the Post, Redirect, Get (PRG) pattern in which it is "to help avoid duplicate form submissions and allow web applications to behave more intuitively with browser bookmarks and the reload button" (Wikipedia).

 

A normal ASP.NET Web Form Lifecycle has the following pattern

  1. HTTP GET of "Create.aspx"
  2. HTTP POST of "Create.aspx"
  3. Validation Fails, "Create.aspx" is Re-Rendered
  4. HTTP POST of "Create.aspx"
  5. Item is created, "Create.aspx" is Re-Rendered with confirmation message

The major problems with this Postback pattern, is that hitting the Refresh button of your browser in steps 3 or 5 will re-post your submitted data. Step 5 is more of a problem as it could possibly re-submit that created information. Granted, there are steps that you can take to approach this problem, but this is how default ASP.NET Web Forms are treated.

Taking this same approach within ASP.NET MVC, can be achieved in the same manner by rendering a your "Create" view from your POST action. For example:

  1. HTTP GET of "/products/create", "Create" view is rendered
  2. HTTP POST to "/products/submit"
  3. Validation Fails, "Create" view is rendered
  4. HTTP POST to "/products/submit"
  5. Item is created, "Confirm" view is rendered

As you'll notice, the same problems we had with ASP.NET Web Forms exists with ASP.NET MVC. The really nice option, is that ASP.NET MVC gives you a lot more "freedom" of how the workflow is processed. If we strictly follow the PRG pattern within ASP.NET MVC, it would look something like

  1. HTTP GET of "/products/create", "Create" view is rendered
  2. HTTP POST to "/products/submit"
  3. Validation Fails, redirect to "/products/create", "Create" view is rendered
  4. HTTP POST to "/products/submit"
  5. Item is created, redirect to "/products/confirm", "Confirm" view is rendered

As you'll notice, where we previously could have had issues in step 3 or 5 before, we no longer have issues. If a user presses the Refresh button in either of those steps, they'll not get the lovely "Would you like to resubmit the form data" confirmation as featured below - instead, the page just reloads.

image

To implement this, you'll need 1 controller, 3 action methods, and 2 views. Follow the steps below to achieve this pattern:

   1:  using System.Web.Mvc;
   2:   
   3:  public class ProductsController : Controller
   4:  {
   5:     public ActionResult Create() { ... }
   6:     public ActionResult Submit() { ... }
   7:     public ActionResult Confirm() { ... }
   8:  }

When you implement your Create action, you have to keep in mind that validation may fail and you may need to re-display the form. TempData is best suited for this scenario, and is implemented as such.

 

   1:  public ActionResult Create()
   2:  {
   3:     if (TempData["ErrorMessage"] != null)
   4:     {
   5:        ViewData["ErrorMessage"] = TempData["ErrorMessage"];
   6:        ViewData["Name"] = TempData["Name"];
   7:        ViewData["Price"] = TempData["Price"];
   8:        ViewData["Quantity"] = TempData["Quantity"];
   9:     }
  10:     return RenderView();
  11:  }

Next you'll implement your Submit action. This will perform some validation of the user input data, and if successful will save the info and redirect to the Confirm action. If it is not successful, we'll store the form data into the TempData and redirect to the action Create. This way we mimic maintaining the view's state even if it fails.

   1:  public ActionResult Submit()
   2:  {
   3:      string error = null;
   4:      string name = Request.Form["Name"];
   5:      if (string.IsNullOrEmpty(name))
   6:      {
   7:          error = "Name is empty. ";
   8:      }
   9:      decimal price;
  10:      if (!decimal.TryParse(Request.Form["Price"], out price))
  11:      {
  12:          error += "Price is invalid. ";
  13:      }
  14:      int quantity;
  15:      if (!int.TryParse(Request.Form["Quantity"], out quantity))
  16:      {
  17:          error += "Quantity is invalid.";
  18:      }
  19:   
  20:      if (!string.IsNullOrEmpty(error))
  21:      {
  22:          TempData["ErrorMessage"] = error;
  23:          TempData["Name"] = Request.Form["Name"];
  24:          TempData["Price"] = Request.Form["Price"];
  25:          TempData["Quantity"] = Request.Form["Quantity"];
  26:          return RedirectToAction("Create");
  27:      }
  28:      else
  29:      {
  30:          return RedirectToAction("Confirm");
  31:      }
  32:  }

Something very interesting to note in the above example, is that even though I've pulled all values out of the form into local variables, should either Price or Quantity fail in parsing and I set the TempData to the local variables...I would have lost the user input. So, it's always a smart idea to retrieve the data from the form directly into the TempData. Finally, the Confirm action needs to be implemented.

   1:  public ActionResult Confirm()
   2:  {
   3:      return RenderView();
   4:  }

Now, it's time to create our views:

~/Views/Products/Create.aspx

   1:  <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Create.aspx.cs" Inherits="Views_Products_Create" %>
   2:  <html xmlns="http://www.w3.org/1999/xhtml">
   3:  <head runat="server">
   4:      <title>Create Product</title>
   5:  </head>
   6:  <body>
   7:      <% using (Html.Form<ProductsController>(c => c.Submit())) { %>
   8:      <% if (!string.IsNullOrEmpty((string) ViewData["ErrorMessage"])) { %>
   9:          <div style="color:Red;">
  10:              <%= ViewData["ErrorMessage"] %>
  11:          </div>
  12:      <% } %>
  13:      Name: <%= Html.TextBox("Name", ViewData["Name"]) %><br />
  14:      Price: <%= Html.TextBox("Price", ViewData["Price"]) %><br />
  15:      Quantity: <%= Html.TextBox("Quantity", ViewData["Quantity"]) %><br />
  16:      <%= Html.SubmitButton("submitButton", "Save") %>
  17:      <% } %>
  18:  </body>
  19:  </html>

~/Views/Products/Confirm.aspx

   1:  <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Confirm.aspx.cs" Inherits="Views_Products_Confirm" %>
   2:  <html xmlns="http://www.w3.org/1999/xhtml">
   3:  <head id="Head1" runat="server">
   4:      <title>Confirm Create Product</title>
   5:  </head>
   6:  <body>
   7:      Thanks for creating your product. 
   8:      <%= Html.ActionLink<ProductsController>(c => c.Create(), "Click here") %> to create a new one.
   9:  </body>
  10:  </html>

And that's it. As you can see from the Create view, when writing our textboxes, we give them a default value from the ViewData. You can download the sample application with this pattern running here. Please let me know of any suggestions or issues.

kick it on DotNetKicks.com


Currently rated 4.5 by 4 people

  • Currently 4.5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Comments

May 9. 2008 15:57

Pingback from weblogs.asp.net

ASP.NET MVC - Using Post, Redirect, Get Pattern - eWorld.UI - Matt Hawley

weblogs.asp.net

May 9. 2008 17:24

Could you make the body text lighter grey? I can almost read it.

Rik Hemsley

May 9. 2008 20:49

Pingback from alvinashcraft.com

Dew Drop - May 9, 2008 | Alvin Ashcraft's Morning Dew

alvinashcraft.com

May 9. 2008 21:52

you beat me to this post! Smile

Great stuff, I will definitely point to this in the future.

Ben Scheirman

May 9. 2008 21:54

Great Post! One quick question - what do you use to display your code in your blog posts? Is that a LiveWriter plugin? If so, could you kindly post/email me the link?

Thanks!!

Danny Douglass

May 9. 2008 22:04

Woudn't this be easier?

1. HTTP GET of "/products/create", "Create" view is rendered
2. HTTP POST to "/products/submit"
3. Validation Fails, "Create" view is rendered with error message
4. HTTP POST to "/products/submit"
5. Item is created, redirect to "/products/confirm", "Confirm" view is rendered

This way you don't need to store anything in your temp data between create atempts and you don't get a repost on the confirm screen. A refresh at step 3 will only re-submit bad data, which will fail and they will get the create screen, so the only thing that could be lost is any data they have updated in the form before refreshing.

Mike

May 9. 2008 22:40

@Ben - Smile I'd still like to see your thoughts, drop me an email when you post.

@Danny - yes, it's a Live Writer plugin, just search on gallery.live.com (gallery.live.com/liveItemDetail.aspx)

@Mike - yes, that is perfectly a viable solution, I even stated you could do this. However, the reason of this post was to introduce you to PRG pattern as another alternative. It ultimately comes down to are you a HTTP purist or not, as a HTTP purist would say PRG is the method you should use.

matthaw

May 9. 2008 23:03

Did the spec review give you the idea to write this post? ;)

Haacked

May 9. 2008 23:40

@Phil - no, its been on the back of mind since we started using this pattern in CodePlex Smile

matthaw

May 10. 2008 00:10

I don't see why this pattern wasn't possible with "regular" ASP.NET. All TempData does is put stuff into Session.

foobar

May 10. 2008 00:54

@foobar - I kinda called that out. But you are correct, you could do this in ASP.NET web forms, but unfortunately it's not easy, and just much easier for people to use the Postback model.

matthaw

May 10. 2008 01:55

Not easy? I'd say it's pretty trivial, actually.

foobar

May 10. 2008 03:04

Pingback from hsidev.wordpress.com

ASP.NET MVC Resources « HSI Developer Blog

hsidev.wordpress.com

May 10. 2008 10:21

I second Rik Hemsley. Text color is extremely dark. And funky, too--797b7a. Why not make it even lighter? How about eeeeee? And make it smaller, too, please.

barfoo

May 10. 2008 10:44

@Rik / @barfoo, what text are you referring to that you need lighter? Drop me a line.

matthaw

May 12. 2008 12:22

Pingback from blog.cwa.me.uk

Reflective Perspective - Chris Alcock » The Morning Brew #91

blog.cwa.me.uk

May 12. 2008 19:53

Please, people stop this "light text on white background" nonsense. It is not cool. It is so hard to read..

zed

May 13. 2008 10:12

I must admin, I don't follow what's special here. Apparently I've been using the long lost PRG pattern since the early ASP.NET betas:

- Create.aspx button SubmitButton is clicked
- if (! IsValid) return; causes the page to re-render with a validator error message
- Create.aspx button SubmitButton is clicked again
- this time validation passes, item is created, at the end of SubmitButton_Click method we do a Response.Redirect("Confirm.aspx ...")

Looks just as trivially easy to me. Can also do a Server.Transfer to avoid using Session and/or QueryString.

Michael Teper

May 14. 2008 13:49

Pingback from code-inside.de

Wöchentliche Rundablage: ASP.NET MVC, Silverlight 2, TDD, WPF, jQuery… | Code-Inside Blog

code-inside.de

May 14. 2008 18:38

@Michael - doing a return in your button click doesn't do a redirect upon a post. If at that point, you hit F5, you'll get the prompt to resubmit your posted data. Server.Transfer still may not work as your last request from the client is still a POST and not a get.

matthaw

May 15. 2008 07:41

Matt, that's fair enough (re: the return). I stick it in there for safety/completeness, but I usually code the validation on the client as it's friendlier that way.

My point wasn't to dismiss MVC -- I can appreciate the benefits. What I don't follow is why the move away from server controls and towards spaghetti code. That seems backwards to me. For my part, I am waiting for MVC v2 that brings server controls into the fold.

Or maybe I am just missing something, but in that case the "a-ha!" moment hasn't happened yet for me.

Michael Teper

May 20. 2008 01:46

Great article, I was wondering what the best practise was for sending view data from submit to create via a redirect...

One thing that can save a little time (and I'm sure this was left out for clarity) is to use:

BindingHelperExtensions.UpdateFrom(viewData, Request.Form, "create");

To populate the view data, then:

TempData["createViewData"] = viewData;

Before the redirect. Note that I store the validation errors on the view data between those two lines.

Thanks for posting Smile

Harry

June 15. 2008 21:36

thanks

that's very helpful

M.S. Babaei

June 24. 2008 08:59

About Redirect to other web address // html code --

html-lesson.blogspot.com/.../...to-web-addres.html

adobe photoshop

Add comment


 

  Country flag

[b][/b] - [i][/i] - [u][/u]- [quote][/quote]



Live preview

October 8. 2008 03:43



Copyright © 2000 - 2008 , Excentrics World