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

NO IMAGE

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

 

動態建立的列

還是通過一個示例來看下如何在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中進行新增。

 

 

小結

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

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

 

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