My Technical Notes

Wednesday, 22 May 2013

Dynamic Adding of Controls in ASP.NET

Dynamic addition of controls in ASP.NET is tricky because you must follow a certain pattern in order for it to work.

Dynamic control creation and addition to the control-tree is not something that is rare, in fact all data controls such as the GridView, ListView and FormView work this way. These data controls instantiate the templates that you specify. For example, in a GridView, if, when instantiated, it has a row, it will normally use the ItemTemplate template and instantiate that in a container.

There are some principles/patterns which we must follow when adding controls dynamically:

  • Store number of rows in ViewState This is the technique that the GridView uses. The GridView does not store the DataSet that is displayed, but rather just the number of records that it is being displayed.
  • Add Controls in LoadViewState Since we are storing the number of rows that we will construct in the ViewState, we must then override the LoadViewState method and then add logic that constructs the control tree in exactly the same way that the control-tree was constructed before the post-back. This is an important point because otherwise the postback data and the ViewState will not properly be populated in these controls.
  • Systematic ID-ing of Dynamically Created Controls For controls we add, we may want to give them an ID, and if we choose to do so, we should give them one systematically.
  • Adding of New Controls After Event This is possible and we do not need to remove all the existing controls in order to do this. We only need to add the control to the end and we should maintain the systematic ID-ing of the controls.
  • Removal of a Row After Event If, say, we wanted to remove a control in response to, say, a button click, it is best to remove all controls first and then reconstruct them again. (I am not sure about elevating this pattern to that of a "principle", but it works).

The following is an implementation of a screen which we can add a whole load of TextBoxes and we can remove an arbitrary TextBox, and the state of the textbox should remain. This page was created in an ASP.NET website.

Code Behind


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class _Default : System.Web.UI.Page
{
    public int NumberOfTextBoxes
    {
        get { return ((int?)ViewState["NumberOfTextBoxes"]) ?? 0; }
        set { ViewState["NumberOfTextBoxes"] = value; }
    }

    protected void Page_Load(object sender, EventArgs e)
    {

    }

    protected void AddTextBox_Click(object sender, EventArgs e)
    {
        NumberOfTextBoxes += 1;

        // NumberOfTextBoxes - 1 because if n is the length of the list, n - 1 is the last
        // elements 'index'.
        DynamicControlsContainer.Controls.Add(CreateDynamicControl(NumberOfTextBoxes - 1));
    }

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

    private void LoadDynamicControls(int numberOfTextBoxes)
    {
        for (int i = 0; i < numberOfTextBoxes; ++i)
        {
            DynamicControlsContainer.Controls.Add(CreateDynamicControl(i));
        }
    }

    public Control CreateDynamicControl(int index)
    {
        var control = new Panel()
        {
            ID = "DynamicPanel" + index
        };

        var textBox = new TextBox { ID = "DynamicTextBox" + index };
        var button = new Button { ID = "DynamicButton" + index, Text = "Remove" };
        button.Click += new EventHandler(RemoveButton_Click);

        control.Controls.Add(textBox);
        control.Controls.Add(button);

        return control;
    }

    protected void RemoveButton_Click(object sender, EventArgs e)
    {
        // first remove the parent panel
        var parentPanel = (sender as Control).GetParentControls().OfType<Panel>().First();
        DynamicControlsContainer.Controls.Remove(parentPanel);
        // get all the remaining textual values
        var values = DynamicControlsContainer.GetDescendentControls().OfType<TextBox>().Select(x => x.Text).ToList();
        // clear the controls container (which destroys the viewstate), rebuild from scratch, and re-populate
        DynamicControlsContainer.Controls.Clear();
        NumberOfTextBoxes = values.Count();
        LoadDynamicControls(NumberOfTextBoxes);
        PopulateData(values);
    }

    private void PopulateData(IList<string> values)
    {
        var textBoxes = DynamicControlsContainer.GetDescendentControls().OfType<TextBox>();

        int index = 0;
        foreach (var textBox in textBoxes)
        {
            textBox.Text = values[index];
            index++;
        }
    }
}

Code Behind


<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeFile="Default.aspx.cs" Inherits="_Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <asp:Panel ID="DynamicControlsContainer" runat="server">
    
    </asp:Panel>

    <asp:Button runat="server" ID="AddTextBox" OnClick="AddTextBox_Click" Text="Add TextBox" />
</asp:Content>

If you copy and paste the code into a new project (into the Default.aspx page) remember to define / import the GetDescendentControls(), GetParentControls() extension methods and also delete the generated designer.cs file and then Convert to web application.

No comments: