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



MVC UI Validation Framework

April 2, 2008 16:36 by matthaw

The last few days I've been working on a MVC UI validation framework that closely follows that of ASP.NET. While my implementation is only in it's baby steps, I decided I would post it to the MVC Contrib project, and follow as much standards as what they have. Overall, the framework is very simple and straight forward in it's use.

Here were my requirements:

  1. It must closely match the ASP.NET UI validation framework.
  2. It should support all of the ASP.NET validators in their minimalistic implementation.
  3. It should require very minimal script inclusion or code to make this happen.
  4. It should only by a client side framework as you should be protecting your data on the server side through normal validations.

What I came up with, includes:

  1. It utilizes the ASP.NET UI validation framework by leveraging the WebUIValidation.js file (emitted through WebResource.axd).
  2. It supports all of the validators, with extended support for validation groups:
    1. RequiredValidator
    2. RegularExpressionValidator
    3. RangeValidator (currently does not support Date or Currency data types)
    4. CompareValidator
    5. CustomValidator
  3. It exposes 2 script inclusion calls, and 1 form validation setup call
    1. ValidatorRegistrationScripts() - Will render the WebUIValidation.js script, and scripts for form validation upon submission. This should be placed within the head tag.
    2. ValidatorInitializationScripts() - Will render all of the "expando" attributes for all validators upon the page and any initialization calls to set things up. This should be placed at the very end of your page before the closing body tag.
    3. FormValidation() - Will return an IDictionary object containing the "onsubmit" attribute. This should be called when creating your <form> tag.
  4. It is only a client-side framework leveraging ASP.NET's WebUIValidation.js

Since you now have an idea of the requirements and what was met, here's an example of it in action:

   1:  <%@ Import Namespace="MvcContrib.UI.Html" %>
   2:  <html>
   3:     <head>
   4:        <%= Html.Form().ValidatorRegistrationScripts() %>
   5:     </head>
   6:     <body>
   7:        <% using(Html.Form<MyController>(c => c.Save(), FormMethod.Post, Html.Form().FormValidation())) { %>
   8:        First Name: <%= Html.TextBox("firstName") %>
   9:        <%= Html.Form().RequiredValidator("firstNameRequired", "firstName", "First Name is Required.") %>
  10:        <%= Html.Form().RegularExpressionValidator("firstNameRegex", "firstName", "[a-zA-Z]*", "First Name can only contain letters.") %>
  11:        <br />
  12:        Age: <%= Html.TextBox("age") %>
  13:        <%= Html.Form().RequiredValidator("ageRequired", "age", "Age is Required.") %>
  14:        <%= Html.Form().RegularExpressionValidator("ageRegex", "age", "[0-9]*", "Age can only be numeric.") %>
  15:        <%= Html.Form().RangeValidator("ageRange", "age", "18", "35", ValidationDataType.Integer, "Age is not in target range of 18-35.") %>
  16:        <br />
  17:        <%= Html.SubmitButton("submit", "Save") %>
  18:        <% } %>
  19:        <%= Html.Form().ValidatorInitializationScripts() %>
  20:     </body>
  21:  </html>

At this point, I've posted it as a patch to MVC Contrib project, and hoping they apply it Smile. If you would like to get your hands on it, check out the patches page, mine is #1063. Please let me know your input, I tried making it as simple as possible to use, and I believe I've achieved that.

Update: My patch has been applied by Eric :) Get the latest build (>= 0.0.1.96) it if you'd like to check this out. Also, let me know of any issues you may find by logging bugs on the CodePlex site.



FogBugz getting an API & VS2005 Plugin!

December 22, 2006 16:29 by matthaw

Yes, the title says it all. FogBugz is getting an exposed API (oddly enough feels like WebServices but isn't even close - rar!) in the next version. The good news is there's a beta version of this API already out that enables a new Visual Studio 2005 Plugin to see your cases by your various filters. I've been wanting something like this for home for quite awhile, so I'm quite happy to see this finally exists. I would have posted sooner about this, but it was a rocky start for the API as it's already on (my version #, not there's) 1.2 due to some bugs not allowing me or many others to work with it. So definately check this out if you're using FogBugz and Visual Studio 2005 & hate to have a separate IE window open to manage your bugs.



Microsoft AJAX & Google AdSense

December 22, 2006 16:22 by matthaw

I was bitten by this bug that was introduced in Microsoft AJAX RC. It seems that Microsoft AJAX is overriding the default implementation of Date.parse from Javascript with it's own implementation. Normally this isn't a bad idea, but it only accepted a certain set of templates when parsing making Google AdSense break upon loading on the page. Luckily enough, some developer named Cyril has a workaround available. The forum post and his blog entry (in French) have more information. Those visiting my website should no longer have a Javascript error displayed as I've implemented this workaround temporarily.



Released: Excentrics World Server Controls v2.0.5

December 18, 2006 06:15 by matthaw

Version 2.0.5 of Excentrics World Server Controls has just been released targeting Microsoft AJAX Extensions, RC1 to ensure continued full compatibility as well as some other bug fixes. It should be noted that with this release, the control set will no longer be compatible with any prior versions of Microsoft AJAX Extensions. The items that were added/fixed are:

  • Added disabling auto-complete for the calendar/timepicker textbox.
  • Fixed issue where supplying less decimal characters than the max specified would throw an exception upon postback for the Numeric Box.
  • Fixed issue where readonly was enabled on the Numeric Box and typing over selected text would delete the text.
  • Fixed enabling or disabling the Calendar Popup control via client script multiple times so that the control doesn't display the calendar when clicking on the image.
  • Fixed issue where setting Nullable=True, the visible date was 1/1/1900 on the Calendar Popup.
  • Fixed month/year popup from jumping 10 years down to 5 years since that is what is displayed on the Calendar Popup.

The updated version can be downloaded by visiting http://www.eworldui.net/Download.aspx. The official build number for this release is 2.0.5.2356.



Released: Excentrics World Server Controls v2.0.1

August 2, 2006 09:06 by matthaw

Unfortunately I had some decently nasty bugs crop up that couldn’t wait for v2.1 of my control set to come out. Because of that, I had to release v2.0.1 that contains the following fixes:

  • Fixed issue where AppendDataBoundItems had no effect when binding against the Multi-Text controls.
  • Fixed issue where posting back a page with the ListTransfer controls are present but not visible throws a NULL reference exception.
  • Fixed issue where determining the scrollLeft and scrollTop in IE was causing improper behavior when the page scrolled. This breaks the scrolling div fix for IE.
  • Fixed issue where disabling the CalendarPopup and TimePicker controls upon a postback with the Image displayed, didn't actually disable the image.
  • Fixed issue where it's possible to receive a NULL reference exception in the CalendarPopup or TimePicker when calling the Clear or Reset methods.

While I don’t normally do quick releases like this, I’d rather have a more solid product in the end. Unfortunately these items were not identified sooner and had to wait until the public took a crack at the RTM bits, but I digress. Official build number for this is 2.0.1.2222.

The updated version can be downloaded by visiting http://www.eworldui.net/Download.aspx





Copyright © 2000 - 2024 , Excentrics World