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


    提出问题

    昨天一位网友提出了这么一个问题:动态创建Disabled的文本输入框,页面回发时修改其文本属性无效:

     

    分析问题

    为了更清楚的分析和解决问题,我们先把代码和运行效果展示一下。

    <f:PageManager ID="PageManager1" runat="server"></f:PageManager>
    <f:SimpleForm runat="server" ID="SimpleForm1">
    </f:SimpleForm> 
    <f:Button ID="btnSetValue" runat="server" OnClick="btnSetValue_Click" Text="赋值"></f:Button>

    前台代码很简单:

    1. 一个表单SimpleForm1,后台会动态添加控件到这里面来

    2. 一个按钮,点击回发

    protected void Page_Init(object sender, EventArgs e)
    {
        TextBox t = new TextBox()
        {
            ID = "Text1",
            Label = "动态创建的1",
            Text = DateTime.Now.ToString(),
            Enabled = false,
        };
        SimpleForm1.Items.Add(t);
    }
    
    
    protected void btnSetValue_Click(object sender, EventArgs e)
    {
        TextBox t = SimpleForm1.FindControl("Text1") as TextBox;
        t.Text = DateTime.Now.ToString();
    }

    上面是简化后的代码:

    1. 一个Page_Init事件,在其中动态创建一个文本输入框,并添加到SimpleForm1中。

    2. 一个按钮点击事件,找到动态创建的文本输入框,并修改它的值为最新的时间。

    页面显示效果:

    实际运行时发现,点击【赋值】按钮时,页面的文本输入框的值并未改变。

    怀疑是 Enabled=false 的问题

    最开始这位网页也是怀疑 Enabled=false 的问题,所以我就先把代码改为:

    protected void Page_Init(object sender, EventArgs e)
    {
        TextBox t = new TextBox()
        {
            ID = "Text1",
            Label = "动态创建的1",
            Text = DateTime.Now.ToString(),
            Enabled = true,
        };
        SimpleForm1.Items.Add(t);
    }

    测试发现没问题了:

     

    好像还真是这么回事,调试后发现,如果文本输入框被禁用了,文本输入框的值是不会提交到后台的,对比一下。

    启用文本输入框:

     

    禁用文本输入框:

     

    那是不是说,页面回发时,只要我们把禁用的文本输入框值也回发到后台,不就解决问题了。

    是这样的吗?

    这么做的确能解决这个问题。因为 ASP.NET 会查找请求参数中的回发数据,并更新控件的值。

    问题的关键是,这么做合规吗?是否合乎HTML的规范,显示不是的。

    HTML5 Spec - 禁用的表单项不会出现在表单请求中

    参考下这篇文章:https://stackoverflow.com/questions/7357256/disabled-form-inputs-do-not-appear-in-the-request

     HTML5 Spec中明确定义了 disabled 控件的行为:

    1. 禁用的控件不会接收焦点
    2. 禁用的控件在Tab导航中会自动跳过
    3. 禁用的控件不会出现在表单提交的请求参数中

    换个控件测试,发现真的不是 Disabled=false 的问题

    换一种思路,我们测测其他控件,将TextBox换成Label,发现同样的问题:

    protected void Page_Init(object sender, EventArgs e)
    {
        TextBox t = new TextBox()
        {
            ID = "Text1",
            Label = "动态创建的1",
            Text = DateTime.Now.ToString(),
            Enabled = false,
        };
        SimpleForm1.Items.Add(t);
        
        Label l = new Label()
        {
            ID = "Label1",
            Label = "动态创建的Label1",
            Text = DateTime.Now.ToString()
        };
        SimpleForm1.Items.Add(l);
    }
    
    protected void btnSetValue_Click(object sender, EventArgs e)
    {
        TextBox t = SimpleForm1.FindControl("Text1") as TextBox;
        t.Text = DateTime.Now.ToString();
    
        Label l = SimpleForm1.FindControl("Label1") as Label;
        l.Text = DateTime.Now.ToString();
    }

    测试后发现,在点击按钮时,两个控件的值都没有改变。

    因为 Label 控件不算是用户可修改的表单字段,所以表单提交时根本不会将其数据放在请求参数中。说白了这个逻辑和禁用的文本输入框还是很类似的。

    调试了一圈,发现要想解决这个问题,还是要回到动态创建控件上来。

    9 年前我就写过一篇文章,来回顾一下。

     

    回顾 9 年前的一篇文章

    9年前的这篇文章对动态创建控件进行了深入的讲解:https://www.cnblogs.com/sanshi/archive/2012/11/19/2776672.html

    其中 ASP.NET WebForms 页面的生命周期还是值得我们再次学习一遍:

     我们主要关心的是前面 4 个阶段,9 年后我们再来回味一下,能感觉到 WebForms 的底层设计还是很巧妙的:

    1. 实例化阶段:处理页面标签定义和 Page_Init 中代码
    2. 回发 - 加载视图状态:查找页面中的隐藏字段 __VIEWSTATE,并更新控件属性
    3. 回发 - 加载回发数据:查找请求参数中的数据,并更新控件属性(本例中从请求参数中找文本输入框SimpleForm1$Text1的值)
    4. 加载阶段:执行 Page_Load 中的代码

    上面看起来也很清楚,页面第一次加载时,执行如下过程:

    1. 实例化:页面标签 + Page_Init
    2. 加载:Page_Load 

    页面回发时,执行如下过程:

    1. 实例化:页面标签 + Page_Init
    2. 加载视图状态:从页面隐藏字段 __VIEWSTATE 中查找
    3. 加载回发数据:从当前 HTTP 的请求参数中查找
    4. 加载:Page_Load 

    如果对上面几个阶段不陌生,那我就要问一个问题了:

    __VIEWSTATE里面的数据是怎么来的?

    这里有一个非常关键的关键点,在 9 年前的那篇文章中我反复提到:

     

    当控件完成【加载视图状态阶段后,就会立即开始跟踪其视图状态的改变,之后任何对其属性的改变都会影响最终的控件视图状态。

    这句话另一层含义就是:在【加载视图状态阶段之前,对控件属性的改变不会被跟踪,也不会记录到  __VIEWSTATE 中来。

    更加严格的说,上面的说法有点问题,因为页面第一次加载时没有【加载视图状态阶段】,更精确的描述:

    • 页面第一次加载时,将控件添加到层次结构树之后,即开始跟踪状态变化,并记录到 __VIEWSTATE
    • 页面回发时,在【加载视图状态阶段】之后,即开始跟踪状态变化,并记录到 __VIEWSTATE
      • 如果控件是在【加载视图状态阶段】之后添加到层次结构树的话,则在将控件添加到层次结构树之后开始跟踪状态变化,并记录到 __VIEWSTATE

    我们再来看一眼最初的代码:

    protected void Page_Init(object sender, EventArgs e)
    {
        TextBox t = new TextBox()
        {
            ID = "Text1",
            Label = "动态创建的1",
            Text = DateTime.Now.ToString(),
            Enabled = false,
        };
        SimpleForm1.Items.Add(t);
    }

    可以发现问题了:

    1. 页面第一次加载时
      1.  在 Page_Init 中首先对Text1赋值:Text1.Text="2021-10-29 11:10:00"
      2. 但是这个赋值操作是在添加到层次结构树之前进行的,所以Text1.Text值不会被记录到 __VIEWSTATE 中
    2. 10分钟之后,页面回发时
      1. 在 Page_Init 中首先对Text1赋值:Text1.Text="2021-10-29 11:20:00"
      2. 加载视图状态时,从 __VIEWSTATE  中回复 Text1 之前的状态,但是 __VIEWSTATE 中没有找到

    经过上面的详细分析,可以看出,页面第一次加载时,将 Text1 设置为 11:10,页面回发时按道理是应该保持这个值的,但是却被错误的更新为了 11:20 !

    怎么为动态添加控件赋值呢?我们也提出了一个最佳实践:

     

    解决问题

    把上面的逻辑搞清楚了,解决问题就不难了:

    protected void Page_Init(object sender, EventArgs e)
    {
        TextBox t = new TextBox()
        {
            ID = "Text1"
        };
        SimpleForm1.Items.Add(t);
    }
    
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            TextBox t = SimpleForm1.FindControl("Text1") as TextBox;
            t.Label = "动态创建的1";
            t.Enabled = false;
            t.Text = DateTime.Now.ToString();
        }
    }
    
    protected void btnSetValue_Click(object sender, EventArgs e)
    {
    
        TextBox t = SimpleForm1.FindControl("Text1") as TextBox;
        t.Text = DateTime.Now.ToString();
    }

    运行效果:

     

  • 相关阅读:
    Vue Router基础
    Bootstrap4入门
    React性能优化
    Koa,React和socket.io
    RN-进阶
    RN-入门基础
    RN-环境配置
    React高级指引
    React基础概念
    实现A-Z滑动检索菜单
  • 原文地址:https://www.cnblogs.com/sanshi/p/15479841.html
Copyright © 2020-2023  润新知