eWorld.UI - Matt Hawley

Ramblings of Matt

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

May 8, 2008 23:51 by matthaw
In addition to blogging, I’m also using Twitter. Follow me @matthawley

 

Note: This post has been updated to work with MVC 2 RTM. The example code is a bit smelly (use POCOs, please!) but the workflow is what you should be mostly concerned about. 

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 View();
  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 View();
   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:  <%@ Import Namespace="System.Web.Mvc.Html" %>
   3:  <html xmlns="http://www.w3.org/1999/xhtml">
   4:  <head runat="server">
   5:      <title>Create Product</title>
   6:  </head>
   7:  <body>
   8:      <% using (Html.BeginForm("Submit", "Products")) { %>
   9:      <% if (!string.IsNullOrEmpty((string) ViewData["ErrorMessage"])) { %>
  10:          <div style="color:Red;">
  11:              <%= ViewData["ErrorMessage"] %>
  12:          </div>
  13:      <% } %>
  14:      Name: <%= Html.TextBox("Name", ViewData["Name"]) %><br />
  15:      Price: <%= Html.TextBox("Price", ViewData["Price"]) %><br />
  16:      Quantity: <%= Html.TextBox("Quantity", ViewData["Quantity"]) %><br />
  17:      <input type="submit" value="Save" />
  18:      <% } %>
  19:  </body>
  20:  </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:      <a href="<%= Url.Action("Create") %>">Click here</a> 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



Comments

May 13. 2008 23:57

Pingback from weblogs.asp.net

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

weblogs.asp.net

May 14. 2008 01:24

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

Rik Hemsley

May 14. 2008 04:49

Pingback from alvinashcraft.com

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

alvinashcraft.com

May 14. 2008 05:52

you beat me to this post! Smile

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

Ben Scheirman

May 14. 2008 05: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 14. 2008 06: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 14. 2008 06: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 14. 2008 07:03

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

Haacked

May 14. 2008 07:40

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

matthaw

May 14. 2008 08: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 14. 2008 08: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 14. 2008 09:55

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

foobar

May 14. 2008 11:04

Pingback from hsidev.wordpress.com

ASP.NET MVC Resources « HSI Developer Blog

hsidev.wordpress.com

May 14. 2008 18: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 14. 2008 18:44

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

matthaw

May 16. 2008 20:22

Pingback from blog.cwa.me.uk

Reflective Perspective - Chris Alcock  » The Morning Brew #91

blog.cwa.me.uk

May 17. 2008 03:53

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

zed

May 17. 2008 18: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 18. 2008 21:49

Pingback from code-inside.de

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

code-inside.de

May 19. 2008 02: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 19. 2008 15: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 24. 2008 09: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 20. 2008 05:36

thanks

that's very helpful

M.S. Babaei

March 15. 2009 15:47

Pingback from weblogs.manas.com.ar

Brian J. Cardiff » ASP.NET MVC Guidelines: Notifications

weblogs.manas.com.ar

March 24. 2009 06:08

Pingback from lemtoks.dreamhosters.com

ASP.NET MVC — RC1 — Паттерн Post-Redirect-Get «  Test

lemtoks.dreamhosters.com

March 24. 2009 15:14

Yea i like this pattern, even more now

Bayram Çelik

April 5. 2009 18:36

Pingback from weblogs.asp.net

ASP.NET MVC Best Practices (Part 1) - Kazi Manzur Rashid's Blog

weblogs.asp.net

July 4. 2009 14:04

Pingback from wukangrui.net

ASP.NET MVC 最佳实践(二) | 所谓技术 - 小李刀刀博客

wukangrui.net

October 29. 2009 18:05

Pingback from lybecker.com

ASP.NET MVC Best Practices | Anders Lybeckers Weblog!

lybecker.com

November 1. 2009 12:57

Pingback from 51jquery.com

陈超群’s Blog    » 【转】12个asp.net MVC最佳实践

51jquery.com

February 21. 2010 12:25

Pingback from blog.abodit.com

Useful Twitter links Feb 8-Feb 15 2010 «  Ian Mercer

blog.abodit.com

Comments are closed

Copyright © 2000 - 2024 , Excentrics World