[原创]FineUI秘密花园(二十一) — 表格之动态创建列
有时我们需要根据数据来动态创建表格列,怎么来做到这一点呢?本章会详细讲解。
动态创建的列
还是通过一个示例来看下如何在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: }
最佳实践(Updated:2021-10-29)
动态添加控件模式二:
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文件。
注:《FineUI秘密花园》系列文章由三生石上原创,博客园首发,转载请注明出处。文章目录 官方论坛