zoukankan      html  css  js  c++  java
  • [原创]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 年前的一篇文章】动态创建控件的一个坑和解决方案

  • 相关阅读:
    演讲-自我认识
    App Store--心酸的上线路,说说那些不可思议的被拒理由
    100个iOS开发/设计面试题汇总
    APP store 上架过程中碰到的那些坑&被拒的各种奇葩原因整理&审核指南中文版
    iOS图片攻略之:有3x自动生成2x 1x图片
    iOS多语言备选机制
    程序员如何提高自己》
    initWithFrame 和 initWithCoder
    黑客界大拿tombkeeper文章:怎么学好技术成为技术大拿(题目我自拟的)
    程序员常去的国外开发社区
  • 原文地址:https://www.cnblogs.com/sanshi/p/2776672.html
Copyright © 2011-2022 走看看