1. 程式人生 > >from 表單提交

from 表單提交

因為是轉載文章 在此標明出處,以前有文章是轉的沒標明的請諒解,因為有些已經無法找到出處,或者與其它原因。

如有冒犯請聯絡本人,或刪除,或標明出處。

因為好的文章,以前只想收藏,但連線有時候會失效,所以現在碰到好的直接轉到自己這裡。

Form(表單)對於每個WEB開發人員來說,應該是再熟悉不過的東西了,可它卻是頁面與WEB伺服器互動過程中最重要的資訊來源。 雖然Asp.net WebForms框架為了幫助我們簡化開發工作,做了很完美的封裝,讓我們只需要簡單地使用服務端控制元件就可以直接操作那些 HTML表單元素了。但我認為了解一些基礎的東西,可以使我們不必束縛在WebForms框架上,以及遇到一些奇怪問題時, 可以更從容地解決它們。

今天,我將和大家來聊聊表單,這個簡單又基礎的東西。我將站在HTML和單純的Asp.net框架的角度來解釋它們的工作方式, 因此,本文不演示WebForms伺服器控制元件的相關內容。

回到頂部

簡單的表單,簡單的處理方式

好了,讓我們進入今天的主題,看看下面這個簡單的HTML表單。

<form action="Handler1.ashx" method="post" >
<p>客戶名稱: <input type="text" name="CustomerName" style="width: 300px" /></p>
<p>客戶電話: <
input type="text" name="CustomerTel" style="width: 300px" /></p> <p><input type="submit" value="提交" /></p> </form>

在這個HTML表單中,我定義了二個文字輸入框,一個提交按鈕,表單將提交到Handler1.ashx中處理,且以POST的方式。
注意哦,如果我們想讓純靜態頁面也能向伺服器提交資料,就可以採用這樣方式來處理:將action屬性指向一個伺服器能處理的地址。

說明:當我們使用WebForms的伺服器表單控制元件時,一般都會提交到頁面自身來處理(action屬性指向當前頁面), 這樣可以方便地使用按鈕事件以及從伺服器控制元件訪問從瀏覽器提交的控制元件輸入結果。
如果在URL重寫時,希望能在頁面回傳時保持URL不變,即:action為重寫後的URL,那麼可以Page類中執行以下呼叫:

Form.Action = Request.RawUrl;    // 受以下版本支援:3.5 SP1、3.0 SP1、2.0 SP1

好了,我們再回到前面那個HTML表單,看一下如果使用者點選了“提交”按鈕,瀏覽器是如何把表單的內容發出的。 在此,我們需要Fiddler工具的協助,請在提交表單前啟動好Fiddler。我將這個表單的提交請求過程做了如下截圖。

上圖是將要提交的表單的輸入情況,下圖是用Fiddler看到的瀏覽器發出的請求內容。

在這張圖片中,我們可以看到瀏覽器確實將請求發給了我前面在action中指定的地址,且以POST形式發出的。 表單的二個控制元件的輸入值放在請求體中,且做了【編碼】處理,編碼的方式用請求頭【Content-Type】說明, 這樣,當服務端收到請求後,就知道該如何讀取請求的內容了。 注意:表單的資料是以name1=value1&name2=value2 的形式提交的,其中name,value分別對應了表單控制元件的相應屬性。

我們還可以在Fiddler中,將檢視切換到WebForms選項卡,這樣能更清楚地只檢視瀏覽器提交的資料,如下圖。

看了客戶端的頁面和請求的內容,我們再來看看在服務端如何獲取瀏覽器提交的表單的輸入吧,程式碼如下:

string name = context.Request.Form["CustomerName"];
string tel = context.Request.Form["CustomerTel"];

程式碼很簡單,直接根據表單控制元件的name屬性訪問Request.Form就可以了。

回到頂部

表單提交,成功控制元件

我們再來看一下瀏覽器是如何提交表單的,或者說,瀏覽器在提交表單時,要做哪些事情。

瀏覽器並不是將所有的表單控制元件全部發送到伺服器的,而是會查詢所有的【成功控制元件】,只將這些成功控制元件的資料傳送到服務端, 什麼是成功控制元件呢?
簡單地來說,成功控制元件就是:每個表單中的控制元件都應該有一個name屬性和”當前值“, 在提交時,它們將以 name=value 的形式做為提交資料的一部分。
對於一些特殊情況,成功控制元件還有以下規定:
1. 控制元件不能是【禁用】狀態,即指定【disabled="disabled"】。即:禁用的控制元件將不是成功控制元件。
2. 如果一個表單包含了多個提交按鍵,那麼僅當用戶點選的那個提交按鈕才算是成功控制元件。
3. 對於checkbox控制元件來說,只有被使用者勾選的才算是成功控制元件。
4. 對於radio button來說,只有被使用者勾選的才算是成功控制元件。
5. 對於select控制元件來說,所有被選擇的選項都做為成功控制元件,name由select控制元件提供。
6. 對於file上傳檔案控制元件來說,如果它包含了選擇的檔案,那麼它將是一個成功控制元件。
此外,瀏覽器不會考慮Reset按鈕以及OBJECT元素。

注意:
1. 對於checkbox, radio button來說,如果它們被確認為成功控制元件,但沒有為控制元件指定value屬性, 那麼在表單提交時,將會以"on"做為它們的value
2. 如果在服務端讀不到某個表單控制元件的值,請檢查它是否滿足以上規則。

提交方式:在前面的示例程式碼中,我為form指定了method="post",這個提交方法就決定了瀏覽器在提交資料時,通過什麼方式來傳遞它們。
如果是【post】,那麼表單資料將放在請求體中被髮送出去。
如果是【get】,那麼表單資料將會追加到查詢字串中,以查詢字串的形式提交到服務端。
建議:表單通常還是以post方式提交比較好,這樣可以不破壞URL,況且URL還有長度限制。

資料的編碼:前面我將瀏覽器的請求細節用Fiddler做了個截圖,從這個圖中我們可以看到:控制元件輸入的內容並不是直接傳送的, 而是經過一種編碼規則來處理的。目前基本上只會只使用二種編碼規則:application/x-www-form-urlencoded 和 multipart/form-data , 這二個規則的使用場景簡單地說就是:後者在上傳檔案時使用,其它情形則使用前者(預設)。
這個規則是在哪裡指定的呢? 其實form還有個enctype屬性,用它就可以指定編碼規則,當我在VS2008寫程式碼時,會有以下提示:

按照我前面說過的編碼規則選擇邏輯,application/x-www-form-urlencoded做為預設值,所以,一般情況下我們並不用顯式指定。 除非我們要上傳檔案了,那麼此時必須設定enctype="multipart/form-data"

好了,說了這麼一大堆理論,我們再來看一下瀏覽是如何處理表單資料的。這個過程大致分為4個階段:
1. 識別所有的成功控制元件。
2. 為所有的成功控制元件建立一個數據集合,它們包含 control-name/current-value 這樣的值對。
3. 按照form.enctype指定的編碼規則對前面準備好的資料進行編碼。編碼規則將放在請求中,用【Content-Type】指出。
4. 提交編碼後的資料。此時會區分post,get二種情況,提交的地址由form.action屬性指定的。

回到頂部

多提交按鈕的表單

用過Asp.net WebForms框架的人可能都寫過這樣的頁面:一個頁面中包含多個服務端按鈕。處理方式嘛, 也很簡單:在每個按鈕的事件處理器寫上相應的程式碼就完事了,根本不用我們想太多。
不過,對於不理解這背後處理過程的開發人員來說,當他們轉到MVC框架下,可能會被卡住:MVC框架中可沒有按鈕事件! 即使用不用MVC框架,用ashx通用處理器的方式,也會遇到這種問題,怎麼辦?
對於這個問題,本文將站在HTML角度給出二個最根本的解決辦法。

方法1:根據【成功控制元件】定義,我們設定按鈕的name,在服務端用name來區分哪個按鈕的提交:

HTML程式碼

<form action="Handler1.ashx" method="post">
<p>客戶名稱: <input type="text" name="CustomerName" style="width: 300px" /></p>
<p>客戶電話: <input type="text" name="CustomerTel" style="width: 300px" /></p>
<p><input type="submit" name="btnSave" value="儲存" />
    <input type="submit" name="btnQuery" value="查詢" />
</p>
</form>

服務端處理程式碼

// 注意:我們只要判斷指定的name是否存在就可以了。        
if( string.IsNullOrEmpty(context.Request.Form["btnSave"]) == false ) {
    // 儲存的處理邏輯
}
if( string.IsNullOrEmpty(context.Request.Form["btnQuery"]) == false ) {
    // 查詢的處理邏輯
}

方法2:我將二個按鈕的name設定為相同的值(根據前面的成功控制元件規則,只有被點選的按鈕才會提交),在服務端判斷value,示例程式碼如下:

<form action="Handler1.ashx" method="post">
<p>客戶名稱: <input type="text" name="CustomerName" style="width: 300px" /></p>
<p>客戶電話: <input type="text" name="CustomerTel" style="width: 300px" /></p>
<p><input type="submit" name="submit" value="儲存" />
    <input type="submit" name="submit" value="查詢" />
</p>
</form>

string action = context.Request.Form["submit"];
if( action == "儲存" ) {
    // 儲存的處理邏輯
}
if( action == "查詢" ) {
    // 查詢的處理邏輯
}

當然了,解決這個問題的方法很多,我們還可以在提交前修改form.action屬性。 對於MVC來說,可能有些人會選擇使用Filter的方式來處理。最終選擇哪種方法,可根據各自喜好來選擇。
我可能更喜歡直接使用Ajax提交到一個具體的URL,這樣也很直觀,在服務端也就不用這些判斷了。接著往下看吧。

回到頂部

上傳檔案的表單

前面我說到“資料的編碼"提到了form.enctype,這個屬性正是上傳表單與普通表單的區別,請看以下示例程式碼:

<form action="Handler2.ashx" method="post" enctype="multipart/form-data">
<p><input type="text" name="str" value="一個字串,別管它" /></p>
<p>要上傳的檔案1<input type="file" name="file1"/></p>
<p>要上傳的檔案2<input type="file" name="file2"/></p>
<p><input type="submit" value="提交" /></p>
</form>

我將上傳2個小檔案

我們再來看看當我點選提交按鈕時,瀏覽器傳送的請求是個什麼樣子的:

注意我用紅色邊框框出來的部分,以及請求體中的內容。此時請求頭Content-Type的值發生了改變, 而且還多了一個叫boundary的引數,它將告訴服務端:請求體的內容以這個標記來分開。 並且,請求體中每個分隔標記會單獨佔一行,且具體內容為:"--" + boundary, 最後結束的分隔符的內容為:"--" + boundary + "--" 也是獨佔一行。 從圖片中我們還可以發現,在請求體的每段資料前,還有一塊描述資訊。
具體這些內容是如何生成的,可以參考本文後面的實現程式碼。

再來看看在服務端如何讀取上傳的檔案。

HttpPostedFile file1 = context.Request.Files["file1"];
if( file1 != null && string.IsNullOrEmpty(file1.FileName) == false )
    file1.SaveAs(context.Server.MapPath("~/App_Data/") + file1.FileName);

HttpPostedFile file2 = context.Request.Files["file2"];
if( file2 != null && string.IsNullOrEmpty(file2.FileName) == false )
    file2.SaveAs(context.Server.MapPath("~/App_Data/") + file2.FileName);

或者

HttpFileCollection files = context.Request.Files;
foreach( string key in files.AllKeys ) {
    HttpPostedFile file = files[key];
    if( string.IsNullOrEmpty(file.FileName) == false )
        file.SaveAs(context.Server.MapPath("~/App_Data/") + file.FileName);
}

二種方法都行,前者更能體現控制元件的name與服務端讀取的關係,後者在多檔案上傳時有更好的擴充套件性。

安全問題:注意,上面示例程式碼中,這樣的寫法是極不安全的。正確的做法應該是:重新生成一個隨機的檔名, 而且最好能對檔案內容檢查,例如,如果是圖片,可以呼叫.net的一些圖形類開啟檔案,然後"另存"檔案。 總之,在安全問題面前只有一個原則:不要相信使用者的輸入,一定要檢查或者轉換。

回到頂部

MVC Controller中多個自定義型別的傳入引數

前面的所有示例程式碼中都有一個規律:在服務端讀取瀏覽器提交的資料時,都會使用控制元件的name屬性,基本上在Asp.net中就是這樣處理。 但是在MVC中,MS為了簡化讀取表單資料的程式碼,可以讓我們直接在Controller的方法中直接以傳入引數的形式指定, 此時框架會自動根據方法的引數名查詢對應的輸入資料(當然也不止表單資料了)。下面舉個簡單的例子:

<form action="/Home/Submit" method="post">
<p>客戶名稱: <input type="text" name="Name" style="width: 300px" /></p>
<p>客戶電話: <input type="text" name="Tel" style="width: 300px" /></p>
<p><input type="submit" value="提交" /></p>
</form>

Conntroller中的方法的簽名:

public ActionResult Submit(Customer customer)
{
}

public ActionResult Submit(string name, string tel)
{
}

以上二種方法都是可以的,當然了,前者會比較好,但需要事先定義一個Customer類,程式碼如下:

public class Customer
{
    public string Name { get; set; }

    public string Tel { get; set; }
}

如果表單簡單或者業務邏輯簡單,我們或許一直也不會遇到什麼麻煩,以上程式碼能很好的工作。 但是,如果哪天我們有了新的業務需要求,需要在這個表單中同時加上一些其它的內容,例如,要把業務員的資料也一起錄入進去。 其中業務員的實體類定義如下:

public class Salesman
{
    public string Name { get; set; }

    public string Tel { get; set; }
}

Controller的介面需要修改成:

public ActionResult Submit(Customer customer, Salesman salesman)
{
}

這時,HTML表單又該怎麼寫呢?剛好,這二個類的(部分)屬性名稱一樣,顯然,前面表單中的Name,Tel就無法對應了。 此時我們可以將表單寫成如下形式:

<form action="/Home/Submit" method="post">
<p>客戶名稱: <input type="text" name="customer.Name" style="width: 300px" /></p>
<p>客戶電話: <input type="text" name="customer.Tel" style="width: 300px" /></p>
<p>銷售員名稱: <input type="text" name="salesman.Name" style="width: 300px" /></p>
<p>銷售員電話: <input type="text" name="salesman.Tel" style="width: 300px" /></p>
<p><input type="submit" value="提交" /></p>
</form>

注意Controller方法中的引數名與HTML表單中的name是有關係的。

回到頂部

F5重新整理問題並不是WebForms的錯

剛才說到了MVC框架,再來說說WebForms框架。以前時常聽到有些人在抱怨用WebForms的表單有F5的重新整理重複提交問題。 在此我想為WebForms說句公道話:這個問題並不是WebForms本身的問題,是瀏覽器的問題, 只是如果您一直使用WebForms的較傳統用法,是容易產生這個現象的。那麼什麼叫做【傳統用法】呢?這裡我就給個我自己的定義吧: 所謂的WebForms的傳統用法是說:您的頁面一直使用伺服器控制元件的提交方式(postback),在事件處理後,頁面又進入再一次的重現過程, 或者說:當前頁面一直在使用POST方式向當前頁面提交。

那麼如何避開這個問題呢?辦法大致有2種:

1. PRG模式(Post-Redirect-Get),在事件處理後,呼叫重定向的操作Response.Redirect(), 而不要在事件處理的後期再去給一些伺服器控制元件繫結資料項了!
建議:按鈕事件只做一些提交資料的處理,將資料繫結的操作放在OnPreRender方法中處理,而不是寫在每個事件中(遍地開花)。 不過,這種方式下,可能偉大的ViewState就發揮不了太大的作用了,如果您發現ViewState沒用了,在Web.config中全域性關掉後, 又發現很多伺服器控制元件的高階事件又不能用了!嗯,杯具有啊。
這個話題說下去又沒完沒了,到此為止吧,不過,千萬不要以為這種方法是在倒退哦。

2. 以Ajax方式提交表單,請繼續閱讀本文。

回到頂部

以Ajax方式提交整個表單

前面一直在說”瀏覽器提交表單",事實上我們也可以用JavaScript提交表單,好處也有很多,比如前面所說的F5重新整理問題。 以Ajax方式提交表單的更大好處它是非同步的,還可以實現區域性重新整理,這些特性都是瀏覽器提交方式沒有的。 前面我提到表單在提交時,瀏覽器要實現的4個步驟,基本上用JS來完成這個操作也是一樣的。 但是,前面說的步驟好像很麻煩呢,有沒有簡單的方法來實現這個過程呢? 嗯,有的,這裡我將使用JQuery以及jquery.form.js這個外掛來演示這個複雜過程的簡單處理方案。

示例用的HTML表單還是我前面用的程式碼,完全不需要修改:

<form action="Handler1.ashx" method="post" >
<p>客戶名稱: <input type="text" name="CustomerName" style="width: 300px" /></p>
<p>客戶電話: <input type="text" name="CustomerTel" style="width: 300px" /></p>
<p><input type="submit" value="提交" /></p>
</form>

JS程式碼如下:

$(function(){
    $('form').ajaxForm({
        success: function(responseText){
            alert(responseText);
        }
    });
});

是的,就是這麼簡單,只要呼叫ajaxForm()就行了。你也可以傳入任何$.ajax()能接受的引數。
它的作用是:修改表單的提交方式,改成Ajax方式提交。最終當用戶點選“提交”按鈕時,此時不再是瀏覽器的提交行為了, 而是使用Ajax的方式提交,提交的URL以及提交方法就是在FORM中指定的引數。

如果您希望要使用者點選某個按鈕或者連結時,也能提交表單(不經過提交按鈕),那麼可以使用如下方法:

$(function(){
    $("#btnId").click(function(){
        $('form').ajaxSubmit({
            success: function(responseText){
                alert(responseText);
            }
        });
    });
});

變化很小,只需要將ajaxForm修改成ajaxSubmit就OK了。 與ajaxForm()不同,呼叫ajaxSubmit()方法將會立即提交表單。

回到頂部

以Ajax方式提交部分表單

在前面的示例中,我們看到以Ajax方式提交一個表單是非常容易的,它完全模擬了瀏覽器的行為。 不過,有時我們可能需要只提交表單的一部分,為的是更好的區域性更新,那麼又該如何做呢?
假如我有以下表單的一部分,我只希望在使用者某個按鈕時將它提交到服務端:

<div id="divCustomerInfo">
<p>客戶名稱: <input type="text" name="CustomerName" style="width: 300px" /></p>
<p>客戶電話: <input type="text" name="CustomerTel" style="width: 300px" /></p>
</div>

我們可以這樣來提交這部分表單的資料:

$("#btnId").click(function(){
    $.ajax({
        url: "Handler1.ashx", type: "POST",
        data: $('#divCustomerInfo :text').fieldSerialize(),
        success: function(responseText){
            alert(responseText);
        }
    });
    return false;
});

注意關鍵的程式碼行:data: $('#divCustomerInfo :text').fieldSerialize()
注意:此時將由您指定一個【JQuery選擇器】來過濾要提交的控制元件,而不是使用成功控制元件的篩選邏輯。

或者,您也可以使用下面將要介紹的方法,仍然是使用 data: {} 的方式,但需要手工指定資料成員。

回到頂部

使用JQuery,就不要再拼URL了!

JQuery越來越流行,以至於在建立MVC專案時,VS IDE會把JQuery也準備好了,可能MS認為開發WEB專案離不開JQuery了。
的確,JQuery非常方便,尤其是在處理DOM時,不僅如此,在處理AJAX請求時,也非常方便。

不過,有件事卻讓我很納悶:經常看到有人在使用JQuery實現Ajax時,把一堆引數放在URL中傳遞,當然了, 傳送GET請求嘛,這樣做不錯,但是,讓我不解的是:URL是拼接起來的,而且程式碼又臭又長!

如果是一個簡單的引數:"aaa.aspx?id=" + xxId ,這樣也就罷了。但是當一堆引數拼接在一起時,可能一下子還看不清楚到底有幾個什麼樣的引數。 而且經驗豐富一些的開發人員會發現這樣做有時會有亂碼問題,可能網上搜過後,知道還有編碼的工作要處理,於是又加了一堆編碼方法。 到此為止,這段程式碼會讓人看起來很累!

如果您平時也是這樣做的,那麼我今天就告訴您:不要再拼接URL了! $.ajax()的引數不是有個data成員嘛,用它吧。看程式碼:

$.ajax({
    url: "Handler1.ashx", type: "POST",
    data: { id: 2, name: "aaa", tel: "[email protected]#$%^&*()_+-=<>?|", xxxx: "要多少還可以寫多少", encoding: "見鬼去吧。?& :)" },
    success: function(responseText) {
        $("#divResult").html(responseText);
    }
});

你說什麼,只能使用GET ? 哦,那就改一下 type 引數吧。

$.ajax({
    url: "Handler1.ashx", type: "GET",
    data: { id: 2, name: "aaa", tel: "[email protected]#$%^&*()_+-=<>?|", xxxx: "要多少還可以寫多少", encoding: "見鬼去吧。?& :)" },
    success: function(responseText) {
        $("#divResult").html(responseText);
    }
});

看了這個示例,您還會繼續拼URL嗎?