[原創]FineUI秘密花園(二十一) — 表格之動態創建列


有時我們需要根據數據來動態創建表格列,怎么來做到這一點呢?本章會詳細講解。

 

動態創建的列

還是通過一個示例來看下如何在FineUI中動態創建表格列,示例的界面截圖:

image

 

先來看下ASPX的標簽定義:

   1:  <ext:Grid ID="Grid1" runat="server" Width="650px" EnableCheckBoxSelect="true" EnableRowNumber="true"
   2:      Title="表格(動態創建的列)">
   3:  </ext:Grid>

 

ASPX標簽中沒有定義任何列,所有列都是在后台定義的:

   1:  // 注意:動態創建的代碼需要放置於Page_Init(不是Page_Load),這樣每次構造頁面時都會執行
   2:  protected void Page_Init(object sender, EventArgs e)
   3:  {
   4:      InitGrid();
   5:  }
   6:   
   7:  private void InitGrid()
   8:  {
   9:      FineUI.BoundField bf;
  10:   
  11:      bf = new FineUI.BoundField();
  12:      bf.DataField = "Id";
  13:      bf.DataFormatString = "{0}";
  14:      bf.HeaderText = "編號";
  15:      Grid1.Columns.Add(bf);
  16:   
  17:      bf = new FineUI.BoundField();
  18:      bf.DataField = "Name";
  19:      bf.DataFormatString = "{0}";
  20:      bf.HeaderText = "姓名";
  21:      Grid1.Columns.Add(bf);
  22:   
  23:      bf = new FineUI.BoundField();
  24:      bf.DataField = "EntranceYear";
  25:      bf.DataFormatString = "{0}";
  26:      bf.HeaderText = "入學年份";
  27:      Grid1.Columns.Add(bf);
  28:   
  29:      bf = new FineUI.BoundField();
  30:      bf.DataToolTipField = "Major";
  31:      bf.DataField = "Major";
  32:      bf.DataFormatString = "{0}";
  33:      bf.HeaderText = "所學專業";
  34:      bf.ExpandUnusedSpace = true;
  35:      Grid1.Columns.Add(bf);
  36:   
  37:      Grid1.DataKeyNames = new string[] { "Id", "Name" };
  38:  }
  39:   
  40:  protected void Page_Load(object sender, EventArgs e)
  41:  {
  42:      if (!IsPostBack)
  43:      {
  44:          LoadData();
  45:      }
  46:  }
  47:   
  48:  private void LoadData()
  49:  {
  50:      DataTable table = GetDataTable();
  51:   
  52:      Grid1.DataSource = table;
  53:      Grid1.DataBind();
  54:  }

整個代碼結構非常清晰,分為頁面的初始化階段和頁面的加載階段。

在頁面的初始化階段:

  1. 創建一個新的FineUI.BoundField實例;
  2. 設置此實例的DataField、DataFormatString、HeaderText等屬性;
  3. 將新創建的列添加到Grid1.Columns屬性中。

 

頁面的加載階段就是綁定數據到表格,和之前的處理沒有任何不同。

 

動態創建的模板列

模板列的動態創建有點復雜,我們先來看下創建好的模板列:

image

 

ASPX標簽和上面例子一模一樣,就不再贅述。我們來看下動態創建模板列的代碼:

   1:  FineUI.TemplateField tf = new TemplateField();
   2:  tf.Width = Unit.Pixel(100);
   3:  tf.HeaderText = "性別(模板列)";
   4:  tf.ItemTemplate = new GenderTemplate();
   5:  Grid1.Columns.Add(tf);

 

這里的GenderTemplate是我們自己創建的類,這也是本例的關鍵點。

   1:  public class GenderTemplate : ITemplate
   2:  {
   3:      public void InstantiateIn(System.Web.UI.Control container)
   4:      {
   5:          AspNet.Label labGender = new AspNet.Label();
   6:          labGender.DataBinding += new EventHandler(labGender_DataBinding);
   7:          container.Controls.Add(labGender);
   8:      }
   9:   
  10:      private void labGender_DataBinding(object sender, EventArgs e)
  11:      {
  12:          AspNet.Label labGender = (AspNet.Label)sender;
  13:   
  14:          IDataItemContainer dataItemContainer = (IDataItemContainer)labGender.NamingContainer;
  15:   
  16:          int gender = Convert.ToInt32(((DataRowView)dataItemContainer.DataItem)["Gender"]);
  17:         
  18:          labGender.Text = (gender == 1) ? "男" : "女";
  19:      }
  20:  }

GenderTemplate實現了ITemplate接口,其中InstantiateIn在需要初始化模板中控件時被調用:

  1. 創建一個Asp.Net的Label控件實例 (AspNet.Label labGender = new AspNet.Label());
  2. 設置數據綁定處理函數(labGender.DataBinding += new EventHandler(labGender_DataBinding));
  3. 將此Label實例添加到模板容器中(container.Controls.Add(labGender))。

 

之后,在對Label進行數據綁定時:

  1. 首先得到當前Label實例,也即是sender對象;
  2. 獲取Label的命名容器,此容器實現了IDataItemContainer接口;
  3. 將此接口的DataItem強制轉換為DataRowView,因為數據源是DataTable;
  4. 根據數據源的值設置Label的值。

 

上面的兩個示例,我們都把動態創建控件的代碼當時Page_Init函數中,這是為什么呢?

要想明白其中的道理,我們還是要從Asp.Net中動態添加控件的原理說起。

 

太棒了太棒了太棒了

學習Asp.Net的視圖狀態和生命周期

這個話題比較深入,也不大容易理解,建議大家在閱讀本節之前詳細了解Asp.Net的視圖狀態和頁面的生命周期,下面是兩個非常經典的參考文章(本節的部分圖片和文字都來自這兩篇文章):

  1. Understanding ASP.NET View State
  2. 創建動態數據輸入用戶界面

 

Asp.Net頁面的生命周期

從上圖可以看出,Asp.Net頁面的生命周期分為如下幾個階段:

  1. 實例化階段:根據ASPX標簽定義的靜態結構創建控件的層次結構,並會調用頁面的Page_Init事件處理函數。
  2. 加載視圖狀態階段(僅回發):將VIEWSTATE中發現的視圖狀態數據恢復到控件的層次結構中。
  3. 加載回發數據階段(僅回發):將回發的表單數據恢復到控件的層次結構中,如果表單控件的數據發生變化,還有可能在第5個階段觸發相應的事件。
  4. 加載階段:此時控件的層次結構已經創建完畢,並且控件的狀態已經從視圖數據和回發數據中回發,此時可以訪問所有的控件屬性,並會調用頁面的Page_Load事件處理函數。
  5. 觸發回發事件(僅回發)階段:觸發回發事件,比如按鈕的點擊事件、下拉列表的選中項改變事件。
  6. 保存視圖狀態階段:保存所有控件的視圖狀態。
  7. 渲染階段:將所有頁面控件渲染為HTML代碼。

上面的這七個階段是每個Asp.Net開發人員都應該熟悉和掌握的,它可以幫助我們理解頁面中Page_Load和事件處理函數的邏輯關系。

 

注意:上述處理過程不管是在頁面第一次加載還是在頁面回發,都會發生。理解這一點非常重要!

 

動態添加控件的兩種模式

動態添加控件需要在加載視圖狀態和加載回發數據之前進行,因為我們需要能夠在添加控件之后恢復這些數據。所以這個階段就對應了Page_Init處理函數,這也就是為什么上面兩個例子都在此函數中動態添加控件。

 

但是由於在初始化階段時,視圖狀態和回發數據還沒有恢復,因此此時無法訪問存儲在視圖狀態或者回發數據中的控件屬性。所以還有一個常用的模式是在Page_Init中添加控件,在Page_Load中為動態創建的控件設置默認值。

 

下面兩個示例分別展示了動態添加控件的兩種模式。

動態添加控件模式一:

   1:  protected void Page_Init(object sender, EventArgs e)
   2:  {
   3:      AspNet.Label lab = new AspNet.Label();
   4:      lab.ID = "Label1";
   5:      lab.Text = "Label1";
   6:   
   7:      Form.Controls.Add(lab);
   8:  }
   9:   
  10:  protected void Page_Load(object sender, EventArgs e)
  11:  {
  12:      
  13:  }

 

動態添加控件模式二:

   1:  protected void Page_Init(object sender, EventArgs e)
   2:  {
   3:      AspNet.Label lab = new AspNet.Label();
   4:      lab.ID = "Label1";
   5:   
   6:      Form.Controls.Add(lab);
   7:  }
   8:   
   9:  protected void Page_Load(object sender, EventArgs e)
  10:  {
  11:      if (!IsPostBack)
  12:      {
  13:          AspNet.Label lab = Form.FindControl("Label1") as AspNet.Label;
  14:          lab.Text = "Label1";
  15:      }
  16:  }

第二種模式是在初始化階段添加動態控件,然后在加載階段(!IsPostBack)設置控件的默認值。

 

 

 

 

錯誤使用動態添加控件的例子一

你可能會想上例中,為什么要將設置控件默認值的代碼放在 !IsPostBack 邏輯塊中,下面就來看下不放在!IsPostBack 邏輯塊中的例子。

首先看下ASPX標簽結構:

   1:  <form id="form1" runat="server">
   2:  <asp:Button ID="Button1" Text="Change Text" OnClick="Button1_Click" runat="server" />
   3:  <asp:Button ID="Button2" Text="Empty Post" runat="server" />
   4:  <br />
   5:  </form>

再看下后台的初始化代碼:

   1:  protected void Page_Init(object sender, EventArgs e)
   2:  {
   3:      AspNet.Label lab = new AspNet.Label();
   4:      lab.ID = "Label1";
   5:   
   6:      Form.Controls.Add(lab);
   7:  }
   8:   
   9:  protected void Page_Load(object sender, EventArgs e)
  10:  {
  11:      AspNet.Label lab = Form.FindControl("Label1") as AspNet.Label;
  12:      lab.Text = "Label1";
  13:  }
  14:   
  15:   
  16:  protected void Button1_Click(object sender, EventArgs e)
  17:  {
  18:      AspNet.Label lab = Form.FindControl("Label1") as AspNet.Label;
  19:      lab.Text = "Changed Label1";
  20:  }

按如下步驟操作:

  1. 第一次打開頁面,顯示的文本是 Label1;
  2. 點擊“Change Text”按鈕,顯示的文本是 Changed Label1;
  3. 點擊“Empty Post”按鈕,顯示的文本是 Label1。

這就不對了,點擊“Empty Post”按鈕時顯示的文本也應該是 Changed Label1,但是上例中文本控件的視圖狀態沒有保持,這是為什么呢?

原因也很簡單,當用戶進行第三步操作(即點擊“Empty Post”按鈕):

  1. 在初始化階段(Page_Init),添加了動態控件Label1;
  2. 根據頁面的生命周期,之后進行的是加載視圖狀態(LoadViewState),此時動態控件Label1的文本是 Changed Label1;
  3. 加載視圖狀態之后就開始跟蹤視圖狀態的變化;
  4. 在加載階段(Page_Load),跟蹤到了控件屬性值的變化,Label1的值就又從Chenged Label1變成了Label1。

 

關鍵點:當控件完成加載視圖狀態階段后,就會立即開始跟蹤其視圖狀態的改變,之后任何對其屬性的改變都會影響最終的控件視圖狀態。

理解這一點非常重要,如果你尚未理解這句話的意思,請多讀幾遍,再多讀幾遍,這句話同時會影響后面介紹的另外兩種動態添加控件的模式。

 

如果你能理解上面提到的過程,說明你已經掌握了Asp.Net的頁面生命周期和ViewState的加載過程了。

 

動態添加控件的另外兩種模式

除了在初始化階段動態添加控件外,還可以再加載階段添加控件。這是因為當把一個控件添加到另一個控件的Controls集合時,所添加的控件的生命周期會立即同步到父控件的生命周期。比如,如果父控件處於初始化階段,則會觸發所添加控件的初始化事件;如果父控件處於加載階段,則會觸發所添加控件的的初始化事件、加載視圖事件、加載回發數據事件以及加載事件。

 

由此,我們就有了另外兩種動態添加控件的模式:

動態添加控件模式三:

   1:  protected void Page_Load(object sender, EventArgs e)
   2:  {
   3:      AspNet.Label lab = new AspNet.Label();
   4:      lab.ID = "Label1";
   5:      lab.Text = "Label1";
   6:      Form.Controls.Add(lab);
   7:  }

 

對於這一種模式,你是否有這樣的疑問?:

如果此標簽的Text屬性在某次Ajax回發時改變了,那么下次Ajax回發時,創建此標簽並賦默認值會不會覆蓋恢復的視圖狀態呢(因為此時已經過了加載視圖狀態階段)?

其實不會這樣的,雖然在Page_Load已經過了加載視圖狀態階段,但是由於此標簽控件尚未添加到控件層次結構中,所以尚未經歷加載視圖狀態階段,只有在Controls.Add之后才會經歷標簽控件的初始化階段、加載視圖狀態階段、加載回發數據階段和加載階段。

 

下面通過一個例子說明,首先看下ASPX標簽結構:

   1:  <form id="form1" runat="server">
   2:  <asp:Button ID="Button1" Text="Change Text" OnClick="Button1_Click" runat="server" />
   3:  <asp:Button ID="Button2" Text="Empty Post" runat="server" />
   4:  <br />
   5:  </form>

后台代碼:

   1:  protected void Page_Load(object sender, EventArgs e)
   2:  {
   3:      AspNet.Label lab = new AspNet.Label();
   4:      lab.ID = "Label1";
   5:      lab.Text = "Label1";
   6:      Form.Controls.AddAt(label2Index, lab);
   7:  }
   8:   
   9:  protected void Button1_Click(object sender, EventArgs e)
  10:  {
  11:      AspNet.Label lab = Form.FindControl("Label1") as AspNet.Label;
  12:      lab.Text = "Changed Label1";
  13:  }

進行如下操作:

  1. 第一次打開頁面,顯示的文本是 Label1;
  2. 點擊“Change Text”按鈕,顯示的文本是 Changed Label1;
  3. 在Page_Load中設置斷點,點擊“Empty Post”按鈕,觀察標簽的Text屬性如下所示。

 

在執行Controls.Add之前,文本值還是Label1:

image

 

在執行Controls.Add之后,文本值從視圖狀態恢復,變成了 Changed Label1:

image

 

 

動態添加控件模式四:

   1:  protected void Page_Load(object sender, EventArgs e)
   2:  {
   3:      AspNet.Label lab = new AspNet.Label();
   4:      lab.ID = "Label1";
   5:      
   6:      Form.Controls.Add(lab);
   7:   
   8:      if (!IsPostBack)
   9:      {
  10:          lab.Text = "Label1";
  11:      }
  12:  }

 

錯誤使用動態添加控件的例子二

如果你認為自己已經掌握了動態添加控件的原理,不妨來看下面這個錯誤的例子,看能否指出其中錯誤的關鍵。

先來看下ASPX標簽結構:

   1:  <form id="form1" runat="server">
   2:  <asp:Button ID="Button2" Text="Empty Post" runat="server" />
   3:  <br />
   4:  </form>

 

在看后台初始化代碼:

   1:  protected void Page_Load(object sender, EventArgs e)
   2:  {
   3:      AspNet.Label lab = new AspNet.Label();
   4:      lab.ID = "Label1";
   5:      if (!IsPostBack)
   6:      {
   7:          lab.Text = "Label1";
   8:      }
   9:   
  10:      Form.Controls.Add(lab);
  11:  }

是不是和動態添加控件模式四比較類似,不過這里的用法卻是錯誤的,你能看出問題所在嗎?

 

來運行一把:

  1. 第一次加載頁面,顯示的文本是Label1;
  2. 點擊“Empty Post”按鈕,顯示的文本為空(這就不對了,應該還是Label1)。

 

為什么會出現這種情況?我們來分析一下:

  • 第一次加載頁面時,設置了文本標簽的默認值,然后添加到控件層次結構中;
  • 添加到控件層次結構后,即開始跟蹤視圖狀態的變化,但是此標簽的Text屬性並沒改變,所以最終沒有保存到視圖狀態中;
  • 點擊按鈕回發時,文本標簽的默認值為空,然后添加到控件層次結構中,在加載視圖狀態階段沒有發現文本標簽的視圖,所以最終顯示為空。

 

那為什么模式四是正確的呢?

簡單來說,修改標簽的Text屬性時已經在跟蹤視圖狀態的改變了,所以這個修改的值被保存了下來;下次回發時又將此值從視圖中恢復了出來。

 

 

錯誤使用動態添加控件的例子三

如果上面的都掌握了,再來看下面這個錯誤的示例,ASPX標簽結構如下:

   1:  <form id="form1" runat="server">
   2:  <asp:Button ID="Button2" Text="Empty Post" runat="server" />
   3:  <br />
   4:  <asp:Label ID="Label2" Text="Label2" runat="server"></asp:Label>
   5:  </form>

后台初始化代碼如下:

   1:  protected void Page_Load(object sender, EventArgs e)
   2:  {
   3:      AspNet.Label lab = new AspNet.Label();
   4:      lab.ID = "Label1";
   5:      lab.Text = "Label1";
   6:      
   7:      int label2Index = Form.Controls.IndexOf(Label2);
   8:   
   9:      Form.Controls.AddAt(label2Index, lab);
  10:   
  11:   
  12:      if (!IsPostBack)
  13:      {
  14:          lab.Text = "Changed Label1";
  15:      }
  16:  }

這段代碼進行了如下處理:

  1. 新創建一個標簽實例Label1,並設置默認值Label1;
  2. 找到頁面上現有標簽Label2在父控件中的索引號;
  3. 將新創建的Label1控件插入Label2所在的位置,也即是將Label2向后移動一個位置;
  4. 在頁面第一次加載時更改新創建標簽Label1的文本為Changed Label1。

 

我們來看下頁面第一個加載的顯示:

image

一切正常,被改變文本值的Label1位於Label2的前面。

 

然后點擊“Empty Post”按鈕,會出現如下情況:

image

為什么本應該保持狀態的Label2,現在的值卻變成了Changed Label1?

 

根本原因是Asp.Net保存保存視圖狀態的方式,是按照控件出現的順序保存的,當然恢復也是按照順序進行的,關於這一特性,我有專門一篇文章詳細闡述。

 

總之,簡單兩句話:

  1. 在Page_Load中動態添加控件時,不要改變現有控件的順序;
  2. 如果想改變現有控件的順序,可以再Page_Init中進行添加。

 

 

或者簡單一句話:在ASP.NET中,所有動態添加控件的代碼都要放到 Page_Init 中進行!

 

 

小結

其實在FineUI中編寫動態創建的表格列非常簡單,但是要想理解其中原理,就不那么簡單了。本篇文章的最后一節詳細描述了動態創建控件的原理,也希望大家能夠細細品味,深入了解Asp.Net的內部運行機制。

下一篇文章我們會詳細講解如何從表格導出Excel文件。

 

注:《FineUI秘密花園》系列文章由三生石上原創,博客園首發,轉載請注明出處。文章目錄 官方論壇


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM