基于viewstate在Page_PreRender中创建动态控件会导致按钮OnClick事件无效

时间:2013-07-19 20:50:27

标签: asp.net

我意识到应该在Page_Load和Page_Init中创建动态控件,以便在控制树中注册它们。

我创建了一个自定义控件,需要在按钮OnClick事件中使用ViewState。然后,此ViewState用于动态创建控件。

由于生命周期将会发生:Page Load - >按钮单击 - > Page PreRender。视图状态在“按钮单击”之前不会更新,因此我在Page PreRender中创建动态控件。但是,创建一个按钮并以编程方式在Page_PreRender中分配OnClick EventHandler不起作用。

有谁知道如何让这个工作?

btn_DeleteTableRow_Click不会触发。这是在CreatePartRows()

中设置的

以下是我的例子:

<asp:UpdatePanel ID="up_RMAPart" runat="server" UpdateMode="Conditional" EnableViewState="true" ChildrenAsTriggers="true">
<ContentTemplate>
    <div class="button" style="width: 54px; margin: 0px; float: right;">
        <asp:Button ID="btn_AddPart" runat="server" Text="Add" OnClick="btn_AddPart_Click" />
    </div>
    <asp:Table ID="Table_Parts" runat="server" CssClass="hor-zebra">
    </asp:Table>
    <div class="clear"></div>
</ContentTemplate>
<Triggers>
    <asp:AsyncPostBackTrigger ControlID="btn_AddPart" EventName="Click" />
</Triggers>

代码背后:

[Serializable]
public struct Part
{
    public string PartName;
    public int Quantity;
    public int PartID;

    public Part(string sPartName, int iQuantity, int iPartID)
    {
        PartName = sPartName;
        Quantity = iQuantity;
        PartID = iPartID;
    }
}

public partial class RMAPart : System.Web.UI.UserControl
{
    private Dictionary<string,Part> m_RMAParts;
    private int m_RowNumber = 0;

    public Dictionary<string, Part> RMAParts
    {
        get
        {
            if (ViewState["m_RMAParts"] != null)
                return (Dictionary<string, Part>)ViewState["m_RMAParts"];
            else
                return null;
        }
        set
        {
            ViewState["m_RMAParts"] = value;
        }
    }

    public int RowNumber
    {
        get
        {
            if (ViewState["m_RowNumber"] != null)
                return Convert.ToInt32(ViewState["m_RowNumber"]);
            else
                return 0;
        }
        set
        {
            ViewState["m_RowNumber"] = value;
        }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            RMAParts = new Dictionary<string, Part>();
            RowNumber = 0;
            RMAParts.Add("PartRow_" + RowNumber.ToString(), new Part());
            RowNumber = 1;
            CreatePartRows();
        }
    }



    protected void Page_PreRender(object sender, EventArgs e)
    {
        CreatePartRows();
    }

    private void CreatePartRows()
    {
        Table_Parts.Controls.Clear();

        TableHeaderRow thr = new TableHeaderRow();

        TableHeaderCell thc1 = new TableHeaderCell();
        thc1.Controls.Add(new LiteralControl("Part"));
        thr.Cells.Add(thc1);

        TableHeaderCell thc2 = new TableHeaderCell();
        thc2.Controls.Add(new LiteralControl("Quantity"));
        thr.Cells.Add(thc2);

        TableHeaderCell thc3 = new TableHeaderCell();
        thc3.Controls.Add(new LiteralControl(""));
        thr.Cells.Add(thc3);

        Table_Parts.Rows.Add(thr);

        foreach (KeyValuePair<string, Part> kvp in RMAParts)
        {
            string[] sKey = kvp.Key.Split('_');

            TableRow tr = new TableRow();
            tr.ID = kvp.Key;

            TableCell tc1 = new TableCell();
            TextBox tb_Part = new TextBox();
            tb_Part.ID = "tb_Part_" + sKey[1];
            tb_Part.CssClass = "textbox1";
            tc1.Controls.Add(tb_Part);
            tr.Cells.Add(tc1);

            TableCell tc2 = new TableCell();
            TextBox tb_Quantity = new TextBox();
            tb_Quantity.ID = "tb_Quanitty_" + sKey[1];
            tb_Quantity.CssClass = "textbox1";
            tc2.Controls.Add(tb_Quantity);
            tr.Cells.Add(tc2);

            TableCell tc3 = new TableCell();
            Button btn_Delete = new Button();
            btn_Delete.ID = "btn_Delete_" + sKey[1];
            btn_Delete.CommandArgument = tr.ID;
            btn_Delete.Click += new EventHandler(btn_DeleteTableRow_Click);                
            btn_Delete.Text = "Remove";
            tc3.Controls.Add(btn_Delete);
            tr.Cells.Add(tc3);

            Table_Parts.Rows.Add(tr);               
        }

    }

    public void Reset()
    {
        Table_Parts.Controls.Clear();
        RMAParts.Clear();
        RowNumber = 0;
        RMAParts.Add("PartRow_" + RowNumber.ToString(), new Part());
        RowNumber = 1;
        CreatePartRows();
    }

    protected void btn_AddPart_Click(object sender, EventArgs e)
    {
        RMAParts.Add("PartRow_" + RowNumber.ToString(), new Part());
        RowNumber++;
    }

    protected void btn_DeleteTableRow_Click(object sender, EventArgs e)
    {
        Button btn = (Button)sender;
        TableRow tr = (TableRow)Table_Parts.FindControl(btn.CommandArgument);
        Table_Parts.Rows.Remove(tr);
        RMAParts.Remove(btn.CommandArgument);
    }        
}

4 个答案:

答案 0 :(得分:3)

确保输入字段的值在回发中持续存在并且引发服务器事件:

  • 使用视图状态跟踪动态创建的控件。
  • LoadViewState not LoadPreRender中重新创建具有相同ID的控件,因为输入字段的值将会丢失)

本答案的其余部分详细说明了我如何修改代码以使其正常工作。

RMAPart.ascx

为方便起见,您可以在.ascx中声明标题行:

<asp:Table ID="Table_Parts" runat="server" CssClass="hor-zebra">
    <asp:TableRow>
        <asp:TableHeaderCell Text="Part" />
        <asp:TableHeaderCell Text="Quantity" />
        <asp:TableHeaderCell />
    </asp:TableRow>
</asp:Table>

RMAPart.ascx.cs

要跟踪动态创建的行,请在视图状态中维护行ID列表:

public partial class RMAPart : System.Web.UI.UserControl
{
    private List<string> RowIDs
    {
        get { return (List<string>)ViewState["m_RowIDs"]; }
        set { ViewState["m_RowIDs"] = value; }
    }

btn_AddPart_Click处理程序中,生成新的行ID并为新行创建控件:

    protected void btn_AddPart_Click(object sender, EventArgs e)
    {
        string id = GenerateRowID();
        RowIDs.Add(id);
        CreatePartRow(id);
    }

    private string GenerateRowID()
    {
        int id = (int)ViewState["m_NextRowID"];
        ViewState["m_NextRowID"] = id + 1;
        return id.ToString();
    }

    private void CreatePartRow(string id)
    {
        TableRow tr = new TableRow();
        tr.ID = id;

        TableCell tc1 = new TableCell();
        TextBox tb_Part = new TextBox();
        tb_Part.ID = "tb_Part_" + id;
        tb_Part.CssClass = "textbox1";
        tc1.Controls.Add(tb_Part);
        tr.Cells.Add(tc1);

        TableCell tc2 = new TableCell();
        TextBox tb_Quantity = new TextBox();
        tb_Quantity.ID = "tb_Quantity_" + id;
        tb_Quantity.CssClass = "textbox1";
        tc2.Controls.Add(tb_Quantity);
        tr.Cells.Add(tc2);

        TableCell tc3 = new TableCell();
        Button btn_Delete = new Button();
        btn_Delete.ID = "btn_Delete_" + id;
        btn_Delete.CommandArgument = id;
        btn_Delete.Click += btn_DeleteTableRow_Click;
        btn_Delete.Text = "Remove";
        tc3.Controls.Add(btn_Delete);
        tr.Cells.Add(tc3);

        Table_Parts.Rows.Add(tr);
    }

btn_DeleteTableRow_Click处理程序中,删除单击的行并更新视图状态:

    protected void btn_DeleteTableRow_Click(object sender, EventArgs e)
    {
        Button btn = (Button)sender;
        TableRow tr = (TableRow)Table_Parts.FindControl(btn.CommandArgument);
        Table_Parts.Rows.Remove(tr);
        RowIDs.Remove(btn.CommandArgument);
    }

挂钩Page_Load并通过创建第一行来启动:

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            Reset();
        }
    }

    public void Reset()
    {
        while (Table_Parts.Rows.Count > 1)
            Table_Parts.Rows.RemoveAt(Table_Parts.Rows.Count - 1);

        ViewState["m_NextRowID"] = 0;
        string id = GenerateRowID();
        RowIDs = new List<string> { id };
        CreatePartRow(id);
    }

覆盖LoadViewState并使用存储在视图状态中的ID重新创建行:

    protected override void LoadViewState(object savedState)
    {
        base.LoadViewState(savedState);

        foreach (string id in RowIDs)
        {
            CreatePartRow(id);
        }
    }
}

处理零件

上面的代码根本不使用您的Part结构。要在业务对象和用户控件之间实际移动数据,可以添加一个公共方法,该方法接受Part集合并使用它来创建行并填充文本框,然后添加另一个读取值的公共方法将文本框放入Part集合。

答案 1 :(得分:1)

没有触发按钮单击,因为在Load事件之后立即调用控件事件。在asp.net生命周期试图调用您的事件时,您的按钮不在控件层次结构中,因此它被删除。请记住,这是一次往返,并且在LoadComplete事件触发之前,控件必须存在于回发中,以便调用其事件处理程序。

在PreLoad或Load事件中创建动态控件,您应该没问题(此时您可以访问完整的视图状态,以决定是否需要为该行动态创建删除按钮)

ASP.net页面生命周期文档:http://msdn.microsoft.com/en-us/library/ms178472(v=vs.100).aspx

答案 2 :(得分:0)

我认为罗伯特有正确的答案,但需要更明确的是他正在谈论的。这里有三个页面请求。

  1. 初始页面请求,尚未点击初始按钮。
  2. 按钮点击后回发。页面加载中没有处理。 PreRender调用创建新表行和新按钮,并将按钮单击事件链接到新按钮。
  3. 客户点击新按钮后回发。您需要在Page Load中重新创建动态按钮,以便不会丢弃动态按钮的Click事件。

答案 3 :(得分:0)

同意罗伯特和比尔。

但是要添加到此处,在我看来,只有通过创建自定义控件/ Web服务器控件(继承WebControl类)才能实现此目的,您可以在其中覆盖CreateChildControls方法和{ {1}}方法。我认为这就是你所说的,在你的一条评论中,你将编写一个网格视图版本。