【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

三生石上(FineUI控制元件)發表於2021-11-01

提出問題

昨天一位網友提出了這麼一個問題:動態建立Disabled的文字輸入框,頁面回發時修改其文字屬性無效:

【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

 

分析問題

為了更清楚的分析和解決問題,我們先把程式碼和執行效果展示一下。

<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. 一個按鈕點選事件,找到動態建立的文字輸入框,並修改它的值為最新的時間。

 

頁面顯示效果:

【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

實際執行時發現,點選【賦值】按鈕時,頁面的文字輸入框的值並未改變。

 

懷疑是 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);
}

 

測試發現沒問題了:

【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

 

好像還真是這麼回事,除錯後發現,如果文字輸入框被禁用了,文字輸入框的值是不會提交到後臺的,對比一下。

啟用文字輸入框:

【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

 

禁用文字輸入框:

【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

 

那是不是說,頁面回發時,只要我們把禁用的文字輸入框值也回發到後臺,不就解決問題了。

是這樣的嗎?

這麼做的確能解決這個問題。因為 ASP.NET 會查詢請求引數中的回發資料,並更新控制元件的值。

 

問題的關鍵是,這麼做合規嗎?是否合乎HTML的規範,顯示不是的。

 

HTML5 Spec - 禁用的表單項不會出現在表單請求中

參考下這篇文章:https://stackoverflow.com/questions/7357256/disabled-form-inputs-do-not-appear-in-the-request

【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

 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();
}

 

測試後發現,在點選按鈕時,兩個控制元件的值都沒有改變。

【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

 

因為 Label 控制元件不算是使用者可修改的表單欄位,所以表單提交時根本不會將其資料放在請求引數中。說白了這個邏輯和禁用的文字輸入框還是很類似的。

 

除錯了一圈,發現要想解決這個問題,還是要回到動態建立控制元件上來。

 

9 年前我就寫過一篇文章,來回顧一下。

 

回顧 9 年前的一篇文章

9年前的這篇文章對動態建立控制元件進行了深入的講解:https://www.cnblogs.com/sanshi/archive/2012/11/19/2776672.html

其中 ASP.NET WebForms 頁面的生命週期還是值得我們再次學習一遍:

【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

 我們主要關心的是前面 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 年前的那篇文章中我反覆提到:

【上接 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 !

 

怎麼為動態新增控制元件賦值呢?我們也提出了一個最佳實踐:

【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

 

 

解決問題

把上面的邏輯搞清楚了,解決問題就不難了:

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();
}

 

執行效果:

【上接 9 年前的一篇文章】動態建立控制元件的一個坑和解決方案

 

 

相關文章