有時我們需要根據數據來動態創建表格列,怎么來做到這一點呢?本章會詳細講解。
動態創建的列
還是通過一個示例來看下如何在FineUI中動態創建表格列,示例的界面截圖:
先來看下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: }
整個代碼結構非常清晰,分為頁面的初始化階段和頁面的加載階段。
在頁面的初始化階段:
- 創建一個新的FineUI.BoundField實例;
- 設置此實例的DataField、DataFormatString、HeaderText等屬性;
- 將新創建的列添加到Grid1.Columns屬性中。
頁面的加載階段就是綁定數據到表格,和之前的處理沒有任何不同。
動態創建的模板列
模板列的動態創建有點復雜,我們先來看下創建好的模板列:
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在需要初始化模板中控件時被調用:
- 創建一個Asp.Net的Label控件實例 (AspNet.Label labGender = new AspNet.Label());
- 設置數據綁定處理函數(labGender.DataBinding += new EventHandler(labGender_DataBinding));
- 將此Label實例添加到模板容器中(container.Controls.Add(labGender))。
之后,在對Label進行數據綁定時:
- 首先得到當前Label實例,也即是sender對象;
- 獲取Label的命名容器,此容器實現了IDataItemContainer接口;
- 將此接口的DataItem強制轉換為DataRowView,因為數據源是DataTable;
- 根據數據源的值設置Label的值。
上面的兩個示例,我們都把動態創建控件的代碼當時Page_Init函數中,這是為什么呢?
要想明白其中的道理,我們還是要從Asp.Net中動態添加控件的原理說起。
學習Asp.Net的視圖狀態和生命周期
這個話題比較深入,也不大容易理解,建議大家在閱讀本節之前詳細了解Asp.Net的視圖狀態和頁面的生命周期,下面是兩個非常經典的參考文章(本節的部分圖片和文字都來自這兩篇文章):
Asp.Net頁面的生命周期
從上圖可以看出,Asp.Net頁面的生命周期分為如下幾個階段:
- 實例化階段:根據ASPX標簽定義的靜態結構創建控件的層次結構,並會調用頁面的Page_Init事件處理函數。
- 加載視圖狀態階段(僅回發):將VIEWSTATE中發現的視圖狀態數據恢復到控件的層次結構中。
- 加載回發數據階段(僅回發):將回發的表單數據恢復到控件的層次結構中,如果表單控件的數據發生變化,還有可能在第5個階段觸發相應的事件。
- 加載階段:此時控件的層次結構已經創建完畢,並且控件的狀態已經從視圖數據和回發數據中回發,此時可以訪問所有的控件屬性,並會調用頁面的Page_Load事件處理函數。
- 觸發回發事件(僅回發)階段:觸發回發事件,比如按鈕的點擊事件、下拉列表的選中項改變事件。
- 保存視圖狀態階段:保存所有控件的視圖狀態。
- 渲染階段:將所有頁面控件渲染為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: }
按如下步驟操作:
- 第一次打開頁面,顯示的文本是 Label1;
- 點擊“Change Text”按鈕,顯示的文本是 Changed Label1;
- 點擊“Empty Post”按鈕,顯示的文本是 Label1。
這就不對了,點擊“Empty Post”按鈕時顯示的文本也應該是 Changed Label1,但是上例中文本控件的視圖狀態沒有保持,這是為什么呢?
原因也很簡單,當用戶進行第三步操作(即點擊“Empty Post”按鈕):
- 在初始化階段(Page_Init),添加了動態控件Label1;
- 根據頁面的生命周期,之后進行的是加載視圖狀態(LoadViewState),此時動態控件Label1的文本是 Changed Label1;
- 加載視圖狀態之后就開始跟蹤視圖狀態的變化;
- 在加載階段(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: }
進行如下操作:
- 第一次打開頁面,顯示的文本是 Label1;
- 點擊“Change Text”按鈕,顯示的文本是 Changed Label1;
- 在Page_Load中設置斷點,點擊“Empty Post”按鈕,觀察標簽的Text屬性如下所示。
在執行Controls.Add之前,文本值還是Label1:
在執行Controls.Add之后,文本值從視圖狀態恢復,變成了 Changed Label1:
動態添加控件模式四:
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: }
是不是和動態添加控件模式四比較類似,不過這里的用法卻是錯誤的,你能看出問題所在嗎?
來運行一把:
- 第一次加載頁面,顯示的文本是Label1;
- 點擊“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: }
這段代碼進行了如下處理:
- 新創建一個標簽實例Label1,並設置默認值Label1;
- 找到頁面上現有標簽Label2在父控件中的索引號;
- 將新創建的Label1控件插入Label2所在的位置,也即是將Label2向后移動一個位置;
- 在頁面第一次加載時更改新創建標簽Label1的文本為Changed Label1。
我們來看下頁面第一個加載的顯示:
一切正常,被改變文本值的Label1位於Label2的前面。
然后點擊“Empty Post”按鈕,會出現如下情況:
為什么本應該保持狀態的Label2,現在的值卻變成了Changed Label1?
根本原因是Asp.Net保存保存視圖狀態的方式,是按照控件出現的順序保存的,當然恢復也是按照順序進行的,關於這一特性,我有專門一篇文章詳細闡述。
總之,簡單兩句話:
- 在Page_Load中動態添加控件時,不要改變現有控件的順序;
- 如果想改變現有控件的順序,可以再Page_Init中進行添加。
或者簡單一句話:在ASP.NET中,所有動態添加控件的代碼都要放到 Page_Init 中進行!
小結
其實在FineUI中編寫動態創建的表格列非常簡單,但是要想理解其中原理,就不那么簡單了。本篇文章的最后一節詳細描述了動態創建控件的原理,也希望大家能夠細細品味,深入了解Asp.Net的內部運行機制。
下一篇文章我們會詳細講解如何從表格導出Excel文件。