同事負責的系統接到抱怨,資料庫被塞入重覆資料,經過一番追查後,發現是使用者的非常態操作所導致,簡單來說--就是送出鈕連按兩下啦!

程式人員或受過訓練的操作員都已經很習慣"執行動作後等待回應"的過程,在按下送出鈕後,就會靜候程式的回應,不會急躁地狂按送出鈕。不過,在實際世界中,並不是每個使用者都會乖乖依你的預期進行操作(所以我們才需要猴子來幫忙測試),遇到缺乏耐性、搞不清狀況或暴怒的使用者,事情的發展就很難預料。

我一直有個錯誤的印象,使用者在按下送出鈕後,瀏覽器就會結束目前的網頁操作,顯示空白等待Postback的結果傳回(其實我已經被狂電過一次,但顯然還是沒學起來)。事實上,在Postback後,瀏覽器會停留在原網頁上,一直到新網頁內容送達為止,在此之前,網頁仍然可以被操作,這給了使用者重覆按下送出鈕的機會。

不信? 我用了以下的ASP.NET網頁來驗證。

<%@ Page Language="C#" %>
<%@ Import Namespace="System.IO" %>
<script runat="server">
    protected void btnSend_Click(object sender, EventArgs e)
    {
        System.Threading.Thread.Sleep(10000);
        using (StreamWriter sw = 
            new StreamWriter("A:\\Output.txt", true))
        {
            sw.WriteLine("{0:yyyy-MM-dd HH:mm:ss.fff} {1}", 
                DateTime.Now, txtData.Text);
            sw.Close();
        }
    }
</script>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Submit Twice</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:TextBox ID="txtData" runat="server"></asp:TextBox>
        <asp:Button ID="btnSend" runat="server" Text="Send" 
        onclick="btnSend_Click" />
    </div>
    </form>
</body>
</html>

在以上的測試中,程式會Delay 10秒才將結果寫入output.txt並傳回結果,在這等待的10秒內使用者有機會從容地多按幾次Send鈕。大約10秒後,output.txt中就會連續冒出多筆記錄,記錄的時間間隔在10秒內,證明了是網頁內容送抵前多按幾次送出鈕就會重覆送出多次Postback。

去網路上爬了一下文,發現大部分建議的解決方式都是用Javascript在按鈕的onclick事件中將按鈕設成disabled防止重覆按下,甚至有人特別寫了只能按一次的送出鈕

但在我的認知中,這件事本當由瀏覽器處理,而不是靠網頁設計師在每一顆送出鈕上加工;就Postback的行為而論,按下後網頁存在的正當性就消失了,只是看守性質,除了默默等待新網頁內容進行輪替外,什麼都不應該做,更別說每按一次送出就Postback一次。

我原本以為這是網站開發者的宿命,是瀏覽器天性使然,心念一轉,改用Firefox測試一下,卻驚奇地發現,Firefox在等待的10秒內,Send鈕可以重覆被按下,卻不會重覆送出POST Request,這才是我期望的Behavior!! 再追加測試了IE8,發現跟IE7一樣會有重覆送出的行為,有點失望。

綜合了以上的研究,我覺得這是個不合理的行為設計(就跟之前IE6中的下拉選單會浮在任何元素最上層一樣),剛才透過Microsoft Connect提了建議,如果有接到回應,再跟大家報告吧!


Comments

# by Shelly

我前二個月才因為處理相同問題傷透了腦筋。 因為是跑流程的東西,一直重覆送出就有可能導至流程壞掉,所以不得不把整個系統畫面的按鈕,加上click事件後,利用div蓋住讓user沒辦法再按!!!orz

# by Johnny

要記得防止重覆 Submit, 這是 ASP.NET 的入門常識。讓瀏覽器來幫你做這個判斷, 這是好壞參半的, 因為我們並不知道 Submit 動作到底有沒有傳送到 Server 端, 瀏覽器也沒辦法百分之百確定。在最壞情況下, 會導致使用者連重新 Submit 的機會都沒有 (即使真的需要這麼做的時候)。 使用 JavaScript 來防止是最常用的做法, 但是並不是最好的做法。我個人一定會在寫入資料庫時再次確認資料庫中有沒有重複的資料, 除非本來就允許。其次, 也可以建立 Session 物件來輔助判斷是否重複輸入。這是我的建議。

# by Jeffrey

to Johnny, "會導致使用者連重新 Submit 的機會都沒有" --> 這個觀點很棒,我之前倒沒想到。(我本來以為遇到這種狀況,就該認賠,乖乖重打一次) 我理想中的做法是瀏覽器在送出POST與收到回應前呈現Busy狀態,使用者如果不耐,再下令中止Busy狀態,之後瀏覽器就允許重送POST。如此,等同於使用者用明確的意思表示要重送,應該就合理了。 我自己設計的編輯UI中,絕大多數有Primary Key保護,重覆送就會Error,大概也是造成我一直忽略這個問題的原因吧。

# by player

你也可以在資料寫進資料庫前 做檢查 如果這個User所寫進資料庫的最後一筆的內容 與目前要新增的內容一樣時 就取消這次的新增 因為在用戶端的任何防堵機制 都是防君子不防小人的

# by Jeffrey

to Player, "用戶端的任何防堵機制, 都是防君子不防小人的" 這句話是經典,如果出了錯無傷大雅,頂多被User譙兩聲的,單靠前端防護還可以說得過去,就算有人惡搞也死不了人。 但如果送出的資料跟錢有關,會動搖國本的,千萬要把防禦重點放在Server-Side。真遇上有心人,隨便掛個GreaseMonkey或Trixie,你做再多Client-Side防禦工事,照樣瞬間蒸發。

# by wangaguo

您好!除了這篇所提的內容之外,想請教相似的問題。 網頁已經送出,但再重覆的reload。 例如: 1.先click一個button,就會post-back,然後網頁顯示結果。 2.這時按Browser的reload,button的click就會被重覆執行。 3.似乎有一些解決的方法,例如redirect,加入Session判斷 4.測試Page.IsPostBack,Browser Reload後還是true,所以不行。 5.測試ViewState,Reload後ViewState又會被Reset,所以不行。 6.您是否有好的解法呢? 7.由於以上的click會讓資料做update的動作,因此檢查key,也無法防止重覆執行,雖然update是不會發生問題,但就很讓人不愉快。 另外有篇design pattern,Post/Redirect/Get http://en.wikipedia.org/wiki/Post/Redirect/Get

Post a comment