【上接 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 控制項的行為:

  1. 禁用的控制項不會接收焦點
  2. 禁用的控制項在Tab導航中會自動跳過
  3. 禁用的控制項不會出現在表單提交的請求參數中

 

換個控制項測試,發現真的不是 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 的底層設計還是很巧妙的:

  1. 實例化階段:處理頁面標籤定義和 Page_Init 中程式碼
  2. 回發 – 載入視圖狀態:查找頁面中的隱藏欄位 __VIEWSTATE,並更新控制項屬性
  3. 回發 – 載入回發數據:查找請求參數中的數據,並更新控制項屬性(本例中從請求參數中找文本輸入框SimpleForm1$Text1的值)
  4. 載入階段:執行 Page_Load 中的程式碼

 

上面看起來也很清楚,頁面第一次載入時,執行如下過程:

  1. 實例化:頁面標籤 + Page_Init
  2. 載入:Page_Load 

 

頁面回發時,執行如下過程:

  1. 實例化:頁面標籤 + Page_Init
  2. 載入視圖狀態:從頁面隱藏欄位 __VIEWSTATE 中查找
  3. 載入回發數據:從當前 HTTP 的請求參數中查找
  4. 載入: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);
}

可以發現問題了:

  1. 頁面第一次載入時
    1.  在 Page_Init 中首先對Text1賦值:Text1.Text=”2021-10-29 11:10:00″
    2. 但是這個賦值操作是在添加到層次結構樹之前進行的,所以Text1.Text值不會被記錄到 __VIEWSTATE 中
  2. 10分鐘之後,頁面回發時
    1. 在 Page_Init 中首先對Text1賦值:Text1.Text=”2021-10-29 11:20:00″
    2. 載入視圖狀態時,從 __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();
}

 

運行效果:

 

 

Tags: