2012年7月4日 星期三

Button 每按一下就累加1的探討

不知道要如何命名比較好
這篇是一個ASP.NET技術:「ViewState」的討論
覺得很是重要,因為常常在抓取控制項的數值時出了錯,報錯常見和ViewState相關
所以特別拿出來研究研究
當然也不是自已的成果啦
只是把高手們的資料看看自已測試一下

先把結果寫出來,有興趣的再往下看吧

結果:

◎以上程式範例AspNet12.aspx,如在頁框下不能操作,請開新視窗操作
◎如果有問題歡迎您提出,dnowba很需要有人和我一起討論

Partial Class AspNet12
    Inherits System.Web.UI.Page
    Dim i As Integer

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        ' ===== 關於Viewstate:按鈕之後,數值累加1 =====
        ' 以下二個結論:
        ' 1、在使用時可以發現
        ' http 是一個 stateless(無狀態) 的協定
        ' 用最原始的response來做是不能累加的,因為
        ' 當傳遞完值後就關閉和server的連接
        ' 而ASP.NET不同處在於在值傳遞過程,都會紀錄每個「控制項」的狀態(viewstate)

        ' 2、如果把button1_click()裡的事件改放到Page_Load()裡
        ' 不管按下什麼控制項,都會發現Page_Load都會被觸發
        ' 這是Page_Load的特性
        ' 是以往傳統程式一個項一個事件,不按就沒有事件被執行的觀念不同
        ' 所以我們常在每個Page_Load裡面寫一段判別式,如下:

        'If Not Page.IsPostBack = True Then
        ' 這邊就是指第一次開啟web應用程序
        ' else
        ' 這邊的敘述句就是指第二、三、四、五…次讀取頁面
        'End If

        If Not Page.IsPostBack = True Then
            ' 也因為控制項的狀態被紀錄過一次了,所以可以用Page.IsPostBack來判定
            ' 就算重新載入了,該控制項的「屬性」還是會被保留住
            Me.Label1.ForeColor = Drawing.Color.Red
            Me.Label1.Text = 0
        End If
        ' 不給初始值=0的話,後面累加1的時候會報錯,即便vb這麼青菜,還是不給加字串的,所以
        ' 我們給他「方便」辯識用,初始值給他設定為 0,讓VB自動把這個「值」當成是「數字」。
        ' c# 就不可以這樣做了,c#較嚴謹,需要設定好每個變數的型別
    End Sub

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
        ' 用最原始的response來做是不能累加的
        ' i = i + 1
        ' Response.Write(i)
        ' 而用控制項的話,因為viewstate的關係,就能累加
        ' 底下用的Cint來將值轉化成int整數,是比較正式的寫法,不寫也行
        Me.Label1.Text = CInt(Me.Label1.Text) + 1
    End Sub
End Class

雖然ASP.NET幫我們做了很多事,電腦自動化的比率愈來愈高
但唯一不變的就是我們始終是要動腦的
ASP.NET幫我們省下的是記一堆函數名稱的時間
但這些函數怎麼用…還是得依賴我們的腦袋瓜
「邏輯力」是電腦沒辦法取而代之的
邏輯力像dnowba這樣不好的話,但至少「偵錯能力」是可以透過經驗不斷累積培養的
image

言歸正傳,如果我們要設計一個網頁,上頭有一個按鈕,按一下就加1,再按一下就變成2…數值累加…要怎麼做呢?這邊就試著用一種邏輯力和偵錯能力的方式一步一步解開這個問題…


測試一:

因為是Button按一下發生才要做,所以我們寫在Button_Click事件裡(沒有寫在Page_Load事件中,程式碼如下:
image

邏輯上有錯嗎…試一下下面的demo網頁,失敗了,因為每按一下的同時,我們定義的變數 i 也因為程式碼中「Dim i As Integer = 0」這句被重新設定為 0 了。

那如果把「Dim i As Integer = 0」改成「Dim i As Integer 」呢?
當然也不行,因為Dim i As Integer ,i 就是一個未使用過的變數,如果像我使用Visual Basic 程式語言的話…那麼VB很好心,還會幫你「預設」這個值是 0 ,所以在VB 裡,有寫沒寫=0都沒差。但是如果是其他程式語言的話…你不給清楚定義的話,就有可能會出錯了。
(VB為什麼會這麼好心:因為Visual Basic 編譯器會使用「型別推斷」(Type Inference) 來判斷未使用 As 子句宣告之區域變數的資料型別,編譯器是根據初始化運算式的型別推斷變數的型別,建議在使用變數的時候,還是宣告好「型別」和「初始值」,免得VB編譯器自動轉成強型別讓程式出錯)

那如果把 i 變數的「初始設定」移到其他事件呢,這樣Button按一下,變數就不會被重新設定了?
這樣子好像可行,我們展開下個測試


測試二

好吧,要複習一下區域變數(Local Variable)的範圍層級。應該是可以把變數宣告在Class這個程序區塊內,把「Dim i As Integer 」放置的位置改變如下圖
image

demo如下,結論還是一樣的…按一下按鈕變數 i 也是不會反應…

如果是設計Windows Application (Windows應用程式) 的話,這個邏輯怎麼會有錯呢???這就是Web Application的關鍵…

若要在 Windows Application 中實作這種控制項相對來說比較簡單,因為 Windows Application 是一種具有狀態記錄的應用程式,在表單中經過 Form_Load 程式產生的控制項,都會被記錄住(有類別層級的區域變數),在後續的程式處理都可以使用。

然而,Web Application 是一個 State-less(無狀態)的應用程式,所有在Web應用程式中產生的控制項,在伺服器產生HTML回傳到 Web Client 後,伺服器就會丟棄這次的處理結果,包含變數與所耗用的資源,讓 Web Server 可以處理更多的用戶端要求的動作,這也表示,控制項的維護,要由 Web Application 來負責,而不是由 Web Server 負責。

那要怎麼修改呢?再往下看吧。


測試三

上面最後的解釋,說得白話一點的話,就是HTTP本身是一個無狀態State-less的協定,用最原始的「response」來做是不能累加的,當傳遞完值後就關閉和server的連接(避免Server端資源的消耗)。

所以上面測試二的網頁不能正常運作,關鍵不在於程式邏輯,而是我們使用了response這個函式,和request一樣,他們都是http的通訊協定。
有興趣再看看我之前寫的關於 Response 和 Request

所幸在以ASP.NET開發的網頁,有著和Windows Application 相同功能,記錄網頁內容各種狀態的函式:ViewState。

ViewState怎麼紀錄網頁狀態的呢?實際測試一下,我們設計一個ASP.NET的web form時,在Client端用瀏覽器檢視網頁,並看看他的程式碼,網頁原始html裡,在傳送到Client端時一定會在<form>後面多了一個ViewState,用來紀錄這個網頁用了哪些「控制項」以及「控制項的屬性」,如下圖,type是hidden,意思就是隱藏的,所以裡頭紀錄了哪些控制項屬性我們是看不到的;value是一堆亂碼,是編碼過的,所以也無法得知屬性的值是什麼。image

不過可以確定一點的是,測試二網頁不是邏輯上有錯,而是顯示的方法有錯:
既然每次的控制項狀態都會被紀錄在ViewState裡,那我們就用控制項Label來接值,因為ViewState會幫我們記住Label的「text屬性」和「text屬性的值」,不用response。後面測試測試:

底下這個是不成功的,因為button在按下後還是先重新定義(Dim)了一次變數i,又把i變回0了。所以我們要把變數 i 的宣告移到別處。image

底下這個寫法也不行,網頁只要一重整,定義的變數就是會重設一次。變數的存留期 (Lifetime)太短了,就只能存活在一段程序中,程序結束就變數就不存在了。所以…就直接利用 ViewState 來存取吧
image

改成這樣呢?把Label1的初始值放在Page_Load事件上面,結果當然就是…Label1 放的位置系統不認帳…不認識Label1需要重新宣告…那就不是控制項啦。
image

把Label1的初始值放在Page_Load裡好了 (不放在Button1_Click事件裡,原因不用解釋了),這樣也是不行的,因為Page_Load是每次程序開始時一定會執行的,和 Windows Application 不一樣。所以每次按下按鈕,就觸發一次Page_Load,label的文字都會被歸零image


測試四

好了,前面測試三的一連串測試,想要用紀錄控制項一舉一動的ViewState來幫我們紀錄每次加1的狀態,最後我們歸納把把Label1的初始值放在Page_Load,卻因為page每次load又把Label1初始化一次,page_load事件裡是我們最常放控制項初始值的地方,講到「page_load」、「初始值」這二個東東,就一定要聯想到「PostBack」的問題。不要問我為什麼?「page_load」+「初始值」=「PostBack」設定。「PostBack」有理論可以講,但是我講不出所以然來…

所以歸根究柢,我們只要在網頁「第一次」運作時設定Label的預設值,當按下Button時,網頁因為是「第二次」回傳了,那Label的值就不會被歸零。

    Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load
        If IsPostBack Then
        Else
            Me.Label1.Text = 0
        End If
    End Sub

    Protected Sub Button1_Click(sender As Object, e As System.EventArgs) Handles Button1.Click
        Me.Label1.Text = Me.Label1.Text + 1
    End Sub

注意一點,就是label的初始值是一定要設定的,前面有提到,自定義的變數可以不用宣告型別,原因在於Visual Basic 編譯器會使用「型別推斷」,所以你至少要告訴他初始值,讓他能轉化成強型別。不給的話就會出現錯誤訊息:輸入字串格式不正確。(系統型別推斷程序當然先推論label.text的值是string型別,如果你告訴他初始值是0,那麼vb會很聰明的設定他是整數型別)image


後記

底下整理一下關於ViewState的種種:

這種狀態管理的函數,我覺得也可以說是一種變數 (如果說Dim X as String,X是區域變數的話,那麼我想ViewState可以說是一種全域變數。說到這樣類型的變數,像Session、Cookies、Application State 也是有著類似的性質,都有著狀態管理的功能。所以當然這個範例也可以改成用session來記,不過各種變數有不同的功能的。

ViewState這個ViewState不是什麼新的東西,在Windows Form上就有使用(就是 form 裡面的 hidden 欄位而已),如今在使用ASP.NET開發的WebForm上面也用上了。我想好處之一就是


前面提到過,ViewState是保存網頁上所有的資料(所有的資料已經輸出並保存在Client Side),這樣的好處就是「不會佔用太多的Server系統資源 (記憶體)」,但缺點就是網頁會變大 (多了一串ViewState,會花比較久的時間Load),像下圖,是大型控制項GridView產生的ViewState,和上面只有一個button時的view比起來,份量大很多的。
image

再來提到上面這一串亂碼,查了一下資料是透過 base64-encoded 方式,這種方式很常運用在網頁資料傳輸上,應該算是「編碼」不算「加密」,目的只是要讓瀏覽者無法直接用肉眼看出裡面的內容,因為演算方式有規則所以可以被「還原(反解)」,防君子不防小人的意思,所以真的有需要防小人的話,也可以把viewstate的value給加密起來(但也意味著更耗資源),方法如下:

首先先在page指示詞上加入 EnableViewStateMAC=”true”  , 如下
< %@Page EnableViewStateMAC="true" %>

再來在 web.config  中加入加密法如3DES或是MD5,如下
< machineKey validation="3DES" />

這樣就可以了. ASP.NET 就會根據你的machineKey去產生hash code
雖然ViewState的內容可以編碼,但是建議還是不要把一些安全性的資料放在這裡.此外,利用這樣子的技術,有個缺點,就是會使得網頁傳遞的時候變慢,因為 form 的內容變多了~

VIEWSTATE肯定是比一般傳統網頁更耗些LOAD時間,所以如果SERVER執行效率不彰的時候(Client端在讀取很慢時),可以檢視一下,檢視的方式

可以用 Trace 來找出網頁的size和ViewState的大小,方法是在Page指示詞後加Trace="true",如下:
< %@ Page Language="vb" Debug="true" Trace="true"%>
記住在測試時開啟就好了,實際上線時關閉才不會耗資源

如果真的不需要的話,不見得每個網頁都需要紀錄VIEWSTATE的,沒有互動的網頁理論上就可以關掉,不過我們在使用ASP.NET撰寫網頁時,預設是開啟的,怎麼手動關閉呢?

如果是某個控制項沒有互動功能的話(GridView沒有分頁、排序等),不需要ViewState:
< asp:GridView EnbleViewState="false" ...%>

如果你的網頁完全沒有互動功能(沒有用到 web control event),不需要ViewState:
<%@ Page EnableViewState="false" ...%>

如果你的整個網站不需要ViewState的話:
在web.config中加入 <Pages EnableViewState="false" ../>

ViewState 是個兩面刃,ViewState 可以很輕易幫我們維護屬性值,不過相對的也增加了面頁的傳輸量,所以可以視實際情形來決定屬性是否要儲存於 ViewState 中。

好像把「簡單」的事情說得太「複雜」了,中間有些過程很白痴,有的過程有沒有說正確自已都搞不大清楚…應該學習一下把複雜的事情講得簡單,這才是高手
反省一下好像,有點鑽牛角尖的東西,算了,畢竟是自已學習的歷程(哈哈,而且是自已第一個暑假的第一個學習日,當然要慶祝一下),硬是分享給大家

1 則留言:

  1. 這問題一只在心中沒解決 原以為這麼簡單 其實不是 真心感謝 給觀念

    回覆刪除

Related Posts Plugin for WordPress, Blogger...
// Dnow Function