提出問題
昨天一位網友提出了這麼一個問題:動態建立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 - 禁用的表單項不會出現在表單請求中
參考下這篇文章:https://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年前的這篇文章對動態建立控制元件進行了深入的講解:https://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(); }
執行效果: