【上接 9 年前的一篇文章】動態創建控制項的一個坑和解決方案
提出問題
昨天一位網友提出了這麼一個問題:動態創建Disabled的文本輸入框,頁面回發時修改其文本屬性無效:
分析問題
為了更清楚的分析和解決問題,我們先把程式碼和運行效果展示一下。
<f:PageManager ID="PageManager1" runat="server"></f:PageManager> <f:SimpleForm runat="server" ID="SimpleForm1"> </f:SimpleForm> <f:Button ID="btnSetValue" runat="server" OnClick="btnSetValue_Click" Text="賦值"></f:Button>
前台程式碼很簡單:
1. 一個表單SimpleForm1,後台會動態添加控制項到這裡面來
2. 一個按鈕,點擊回發
protected void Page_Init(object sender, EventArgs e) { TextBox t = new TextBox() { ID = "Text1", Label = "動態創建的1", Text = DateTime.Now.ToString(), Enabled = false, }; SimpleForm1.Items.Add(t); } protected void btnSetValue_Click(object sender, EventArgs e) { TextBox t = SimpleForm1.FindControl("Text1") as TextBox; t.Text = DateTime.Now.ToString(); }
上面是簡化後的程式碼:
1. 一個Page_Init事件,在其中動態創建一個文本輸入框,並添加到SimpleForm1中。
2. 一個按鈕點擊事件,找到動態創建的文本輸入框,並修改它的值為最新的時間。
頁面顯示效果:
實際運行時發現,點擊【賦值】按鈕時,頁面的文本輸入框的值並未改變。
懷疑是 Enabled=false 的問題
最開始這位網頁也是懷疑 Enabled=false 的問題,所以我就先把程式碼改為:
protected void Page_Init(object sender, EventArgs e) { TextBox t = new TextBox() { ID = "Text1", Label = "動態創建的1", Text = DateTime.Now.ToString(), Enabled = true, }; SimpleForm1.Items.Add(t); }
測試發現沒問題了:
好像還真是這麼回事,調試後發現,如果文本輸入框被禁用了,文本輸入框的值是不會提交到後台的,對比一下。
啟用文本輸入框:
禁用文本輸入框:
那是不是說,頁面回發時,只要我們把禁用的文本輸入框值也回發到後台,不就解決問題了。
是這樣的嗎?
這麼做的確能解決這個問題。因為 ASP.NET 會查找請求參數中的回發數據,並更新控制項的值。
問題的關鍵是,這麼做合規嗎?是否合乎HTML的規範,顯示不是的。
HTML5 Spec – 禁用的表單項不會出現在表單請求中
參考下這篇文章://stackoverflow.com/questions/7357256/disabled-form-inputs-do-not-appear-in-the-request
HTML5 Spec中明確定義了 disabled 控制項的行為:
- 禁用的控制項不會接收焦點
- 禁用的控制項在Tab導航中會自動跳過
- 禁用的控制項不會出現在表單提交的請求參數中
換個控制項測試,發現真的不是 Disabled=false 的問題
換一種思路,我們測測其他控制項,將TextBox換成Label,發現同樣的問題:
protected void Page_Init(object sender, EventArgs e) { TextBox t = new TextBox() { ID = "Text1", Label = "動態創建的1", Text = DateTime.Now.ToString(), Enabled = false, }; SimpleForm1.Items.Add(t); Label l = new Label() { ID = "Label1", Label = "動態創建的Label1", Text = DateTime.Now.ToString() }; SimpleForm1.Items.Add(l); } protected void btnSetValue_Click(object sender, EventArgs e) { TextBox t = SimpleForm1.FindControl("Text1") as TextBox; t.Text = DateTime.Now.ToString(); Label l = SimpleForm1.FindControl("Label1") as Label; l.Text = DateTime.Now.ToString(); }
測試後發現,在點擊按鈕時,兩個控制項的值都沒有改變。
因為 Label 控制項不算是用戶可修改的表單欄位,所以表單提交時根本不會將其數據放在請求參數中。說白了這個邏輯和禁用的文本輸入框還是很類似的。
調試了一圈,發現要想解決這個問題,還是要回到動態創建控制項上來。
9 年前我就寫過一篇文章,來回顧一下。
回顧 9 年前的一篇文章
9年前的這篇文章對動態創建控制項進行了深入的講解://www.cnblogs.com/sanshi/archive/2012/11/19/2776672.html
其中 ASP.NET WebForms 頁面的生命周期還是值得我們再次學習一遍:
我們主要關心的是前面 4 個階段,9 年後我們再來回味一下,能感覺到 WebForms 的底層設計還是很巧妙的:
- 實例化階段:處理頁面標籤定義和 Page_Init 中程式碼
- 回發 – 載入視圖狀態:查找頁面中的隱藏欄位 __VIEWSTATE,並更新控制項屬性
- 回發 – 載入回發數據:查找請求參數中的數據,並更新控制項屬性(本例中從請求參數中找文本輸入框SimpleForm1$Text1的值)
- 載入階段:執行 Page_Load 中的程式碼
上面看起來也很清楚,頁面第一次載入時,執行如下過程:
- 實例化:頁面標籤 + Page_Init
- 載入:Page_Load
頁面回發時,執行如下過程:
- 實例化:頁面標籤 + Page_Init
- 載入視圖狀態:從頁面隱藏欄位 __VIEWSTATE 中查找
- 載入回發數據:從當前 HTTP 的請求參數中查找
- 載入:Page_Load
如果對上面幾個階段不陌生,那我就要問一個問題了:
__VIEWSTATE裡面的數據是怎麼來的?
這裡有一個非常關鍵的關鍵點,在 9 年前的那篇文章中我反覆提到:
當控制項完成【載入視圖狀態階段】後,就會立即開始跟蹤其視圖狀態的改變,之後任何對其屬性的改變都會影響最終的控制項視圖狀態。
這句話另一層含義就是:在【載入視圖狀態階段】之前,對控制項屬性的改變不會被跟蹤,也不會記錄到 __VIEWSTATE 中來。
更加嚴格的說,上面的說法有點問題,因為頁面第一次載入時沒有【載入視圖狀態階段】,更精確的描述:
- 頁面第一次載入時,將控制項添加到層次結構樹之後,即開始跟蹤狀態變化,並記錄到 __VIEWSTATE
- 頁面回發時,在【載入視圖狀態階段】之後,即開始跟蹤狀態變化,並記錄到 __VIEWSTATE
- 如果控制項是在【載入視圖狀態階段】之後添加到層次結構樹的話,則在將控制項添加到層次結構樹之後開始跟蹤狀態變化,並記錄到 __VIEWSTATE
我們再來看一眼最初的程式碼:
protected void Page_Init(object sender, EventArgs e) { TextBox t = new TextBox() { ID = "Text1", Label = "動態創建的1", Text = DateTime.Now.ToString(), Enabled = false, }; SimpleForm1.Items.Add(t); }
可以發現問題了:
- 頁面第一次載入時
- 在 Page_Init 中首先對Text1賦值:Text1.Text=”2021-10-29 11:10:00″
- 但是這個賦值操作是在添加到層次結構樹之前進行的,所以Text1.Text值不會被記錄到 __VIEWSTATE 中
- 10分鐘之後,頁面回發時
- 在 Page_Init 中首先對Text1賦值:Text1.Text=”2021-10-29 11:20:00″
- 載入視圖狀態時,從 __VIEWSTATE 中回復 Text1 之前的狀態,但是 __VIEWSTATE 中沒有找到
經過上面的詳細分析,可以看出,頁面第一次載入時,將 Text1 設置為 11:10,頁面回發時按道理是應該保持這個值的,但是卻被錯誤的更新為了 11:20 !
怎麼為動態添加控制項賦值呢?我們也提出了一個最佳實踐:
解決問題
把上面的邏輯搞清楚了,解決問題就不難了:
protected void Page_Init(object sender, EventArgs e) { TextBox t = new TextBox() { ID = "Text1" }; SimpleForm1.Items.Add(t); } protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { TextBox t = SimpleForm1.FindControl("Text1") as TextBox; t.Label = "動態創建的1"; t.Enabled = false; t.Text = DateTime.Now.ToString(); } } protected void btnSetValue_Click(object sender, EventArgs e) { TextBox t = SimpleForm1.FindControl("Text1") as TextBox; t.Text = DateTime.Now.ToString(); }
運行效果: