ASP.NET CORE MVC - 使用Post, Redirect, Get (PRG)模式

Post/Redirect/Get 簡稱PRG,是一種用來防止表單重複提交數據的一種Web設計模式,典型的重複提交form內容的情況像用戶刷新提交響應頁面等可通過PRG模式來得到避免。

當一個表單通過HTTP POST被請求提交的時候,用戶在服務器端返迴響應期間如果刷新了響應頁面,將會導致原始HTTP POST過來的內容重複提交,可能會導致一些不可預期的結果,比如重複提交數據。

通常我們可以採用PRG模式來迴避重複提交數據問題。PRG模式通過響應頁面Header返回HTTP狀態碼進行頁面跳轉替代響應頁面跳轉過程。PRG模式流程如下圖示:

HTTP 1.1 規範介紹HTTP 303U狀態頁進行跳轉,303狀態能確保會員在瀏覽器端安全地刷新服務器端響應,而不會引起HTTP POST請求重複提交。另外,目前很多商業網站依然繼續使用HTTP 302來響應跳轉,主要考慮到一些版本的瀏覽器不能很好地兼容HTTP1.1規範中的303狀態碼。

HTTP規範摘錄:

302 Found

請求的資源現在臨時從不同的URI響應請求。由於這樣的重定向是臨時的,客戶端應當繼續向原有地址發送以後的請求。只有在Cache-Control或Expires中進行了指定的情況下,這個響應才是可緩存的。

新的臨時性的URI應當在響應的Location域中返回。除非這是一個HEAD請求,否則響應的實體中應當包含指向新的URI的超鏈接及簡短說明。

如果這不是一個GET或者HEAD請求,那麼瀏覽器禁止自動進行重定向,除非得到用戶的確認,因為請求的條件可能因此發生變化。

注意:雖然RFC 1945和RFC 2068規範不允許客戶端在重定向時改變請求的方法,但是很多現存的瀏覽器將302響應視作為303響應,並且使用GET方式訪問在Location中規定的URI,而無視原先請求的方法。狀態碼303和307被添加了進來,用以明確服務器期待客戶端進行何種反應。

303 See Other

對應當前請求的響應可以在另一個URI上被找到,而且客戶端應當採用GET的方式訪問那個資源。這個方法的存在主要是為了允許由腳本激活的POST 請求輸出重定向到一個新的資源。這個新的URI不是原始資源的替代引用。同時,303響應禁止被緩存。當然,第二個請求(重定向)可能被緩存。

新的URI應當在響應的Location域中返回。除非這是一個HEAD請求,否則響應的實體中應當包含指向新的URI的超鏈接及簡短說明。

注意:許多HTTP/1.1版以前的瀏覽器不能正確理解303狀態。如果需要考慮與這些瀏覽器之間的互動,302狀態碼應該可以勝任,因為大多數的瀏覽器處理302響應時的方式恰恰就是上述規範要求客戶端處理303響應時應當做的。

值得注意的是,PRG設計模式並不能適用所有的表單重複提交情況,如以下幾種情況:

  1. 如果用戶返回表單頁面,重新提交表單的情況
  2. 用戶在服務器端響應到達之前,多次點擊提交按鈕的時候。(可通過JavaScript控制提交按鈕點擊次數)
  3. 由於服務器響應緩慢,用戶刷新提交POST請求造成的多次POST請求
  4. 惡意用戶避開客戶端預防多次提交手段,進行重複提交請求

除了PRG設計模式外,另外還有一些其他技術被用在防止表單重複提交的情況下,如客戶端我們可以採取JavaScript防止用戶多次點擊提交按鈕,還可以採用Session記錄用戶當前提交行為等。

引用:http://en.wikipedia.org/wiki/Post/Redirect/Get

 

 


ASP.NET MVC模式通過重新引進已經丟失的,或者至少已經沒有使用很多年的模式,使其本身趨向更簡單和“真實”的HTTP體驗( 原文:The 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 )。其中的一種模式是Post,Redirect,Get(PRG)模式,它可以"避免二次提交和允許web應用程序使用瀏覽器書籤和reload按鈕來更直觀的表現"( Wikipedia ).

一個普通的ASP.NET Web Form生命週期具有下述的模式:

    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

這種Postback模式的主要問題是,在第3步或者第5步點擊瀏覽器的刷新按鈕的時候,將會重新Post你已經提交的數據。第5步還存在的一個問題是它甚至可能會重新提交已經創建了的數據(譯註:即二次提交,創建了兩條相同的數據)。當然,你可以在以上的步驟中處理這些問題,但默認情況下ASP.NET Web Form是這樣對待它的。

將這同樣的情況放到ASP.NET MVC中,可以通過呈現一個來自你的POST action的"Create"視圖來實現同樣的方式。例如:

    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

就和你注意到的一樣,ASP.NET Web Form存在的問題,在ASP.NET MVC中也存在同樣的問題。真正漂亮的選擇是,ASP.NET MVC給予我們更多的"自由"來處理這個流程。如果我們在ASP.NET MVC中嚴格的按照PRG模式,它看起來應該像:

    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

如你所見,我們之前會出現問題的第3步和第5步,不會再存在問題。如果一個用戶以上的任何一個步驟按下刷新按鈕,他們不會看到如下圖所示的可愛的“Would you like to resubmit the form data”的確認信息,而頁面只是重新加載了。

 

要實現這個,你需要1個Controller,3個Action方法,和2個views。跟隨下面的步驟實現這個模式:

using System.Web.Mvc;

public class ProductsController : Controller
{
  public ActionResult Create() { ... }
  public ActionResult Submit() { ... }
  public ActionResult Confirm() { ... }
}

 

當你實現你的Create action,你要記住的是驗證可能是失敗的而你可能要重新顯示這個表單。TempData是最適合在這種情況下使用的,它如下實現:

public ActionResult Create()
{
  if (TempData["ErrorMessage"] != null)
  {
    ViewData["ErrorMessage"] = TempData["ErrorMessage"];
    ViewData["Name"] = TempData["Name"]; 
    ViewData["Price"] = TempData["Price"];
    ViewData["Quantity"] = TempData["Quantity"]; 
  } 
  return RenderView();
}

 

接下來你要實現你的Submit action。這裡將會處理用戶輸入數據的驗證,如果驗證通過則保存數據並重定向到Confirm action。如果驗證失敗將會把表單數據保存到TempData中並重定向到Create action。我們模擬的這個方式在驗證失敗的情況下也會維持視圖數據。

public ActionResult Submit()
{ 
  string error = null;
  string name = Request.Form["Name"];
  if (string.IsNullOrEmpty(name)) 
  { 
    error = "Name is empty. "; 
  } 
  decimal price;
  if (!decimal.TryParse(Request.Form["Price"], out price))  
  { 
    error += "Price is invalid. "; 
  } 
  int quantity; 
  if (!int.TryParse(Request.Form["Quantity"], out quantity))  
  { 
    error += "Quantity is invalid.";  
  } 

  if (!string.IsNullOrEmpty(error)) 
  { 
    TempData["ErrorMessage"] = error;
    TempData["Name"] = Request.Form["Name"]; 
    TempData["Price"] = Request.Form["Price"]; 
    TempData["Quantity"] = Request.Form["Quantity"]; 
    return RedirectToAction("Create"); 
  } 
  else 
  { 
    return RedirectToAction("Confirm"); 
  } 
}

 

這裡註意上面的示例中一些很重要的東西是,儘管我把form中所有的數據放入到本地變量中,可能Price或者Quantity其中任何一個轉換失敗,而我將TempData用上面的本地變量( 譯註:指上面的紅色部分 )賦值...我將丟失用戶輸入的數據。所以,從Form中取得數據並直接存入到TempData中始終是一個聰明的主意。最後,需要實現Confrim action :

public ActionResult Confirm()
{ 
  return RenderView(); //译注:参数为空时将呈现和action同名的view
}

 

現在,是時候來創建我們的視圖了:

~/Views/Products/Create.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Create.aspx.cs" Inherits="Views_Products_Create" %>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head runat="server">
    <title>Create Product</title>
  </head>
  <body>
    <% using (Html.Form<ProductsController>(c => c.Submit())) { %>
      <% if (!string.IsNullOrEmpty((string) ViewData["ErrorMessage"])) { %>
    <div style="color:Red;">
      <%= ViewData["ErrorMessage"] %>
    </div>
    <% } %>
    Name: <%= Html.TextBox("Name", ViewData["Name"]) %><br />
    Price: <%= Html.TextBox("Price", ViewData["Price"]) %><br />
    Quantity: <%= Html.TextBox("Quantity", ViewData["Quantity"]) %><br />
    <%= Html.SubmitButton("submitButton", "Save") %>
    <% } %>
  </body>
</html>

 

~/Views/Products/Confirm.aspx

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

 
就是這樣。你可以從這裡獲取這個模式的示例程序的代碼。

轉載自 ASP.NET MVC - Using Post, Redirect, Get Pattern

 

  • Top