[原创]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:  }

 

最佳实践(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:  }

按如下步骤操作:

  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秘密花园》系列文章由三生石上原创,博客园首发,转载请注明出处。文章目录 官方论坛

 

 

 

九年后再更新

【上接 9 年前的一篇文章】动态创建控件的一个坑和解决方案

posted @ 2012-11-19 08:15  三生石上(FineUI控件)  阅读(17176)  评论(10编辑  收藏  举报