Wednesday, July 4, 2007

Dynamically Created Controls: Dynamic Rows Sample

Sample code: DynamicRowsSample.zip

I described most important problems you may face when dealing with dynamically created controls with in my post (Dynamically Created Controls in ASP.NET).  The sample code in that post demonstrated most of the described problems and showed how to created pages with dynamic layout.

However, the previous sample does not make it obvious how to implement another common scenario with dynamic controls. So, I am going to explain ho to build a web page with dynamically added and removed rows like:

Web Page with Dynamic Rows

One Row

To separate implementation of one row and a web page handling dynamic rows, I placed implementation of a row into a user control in WebUserControl.ascx.  The WebUserControl class exposes events and properties required to communicate with the web page.

    public event EventHandler InsertAbove;
public event EventHandler InsertBellow;
public event EventHandler Remove;

public string Text {
get {
return c_textBox.Text;
}
}

User control fires its events in response to buttons clicks. For example, "Insert Above" button click is handled by:

    protected void InserAboveClick(object sender, EventArgs e) {
if (InsertAbove != null) {
InsertAbove(this, e);
}
}

Web Page Structure


The sample web page consists of 3 areas:



  • A placeholder for dynamically created rows
  • A label to display sample result
  • A button to do a postback (just to play with)

These areas are described in ASP.NET markup as:

<div>
<asp:PlaceHolder runat="server" ID="c_placeHolder">
</asp:PlaceHolder>
<br />
<asp:Label ID="c_resultLabel" runat="server"
Text="Label">
</asp:Label>
<br />
<asp:Button ID="c_postBackButton" runat="server" Text="Do PostBack!" />
</div>

View State and Control Ids


Dynamically created controls must be re-created on each postback and these re-created controls must have the same IDs as they had when were rendered previous time.  You need to know all these details when creating controls on postback.  The best place to keep this information between postbacks is a page view state.


Each dynamically created control must get its unique ID and this ID shall not be changed between postbacks.  Otherwise ASP.NET will not be able to restore a view state of the control and its children.  The sample application allows inserting and removing rows, so it is not good idea to assign IDs each line incrementally.  If you do so, and user clicks "remove" button, the next line will get ID of the removed line and view state will be restored incorrectly. 


I, therefore, allocate each row ID only once and do not re-use the same ID for newly created rows.  The LastControlId is a ViewState backed property which remembers ID of the last row created by a user.

    public int LastControlId {
get {
object v = ViewState["LastControlId"];
return v != null ? (int)v : 0;
}
set {
ViewState["LastControlId"] = value;
}
}

IDs of the currently existing rows are stored in another ViewState backed property: ControlIdList.  This ArrayList holds an ordered list of IDs.  It is used on each postback to create rows with correct IDs.

    public ArrayList ControlIdList {
get {
object v = ViewState["ControlIdList"];
return v != null ? (ArrayList)v : null;
}
set {
ViewState["ControlIdList"] = value;
}
}

Post Back


ASP.NET does not remember dynamically created controls neither in ViewState nor anywhere else.  It is a developer responsibility to reconstruct any dynamically created controls on each postback.  So, you need to create your rows on each postback.  Information stored in ViewState backed properties is enough to create controls with correct IDs.  ASP.NET will do the remaining job on restoring controls state.


In my earlier post I explained that the best place to re-create dynamic controls is LoadViewState method.  Following this recommendation, the sample application creates rows in LoadViewState method by calling CreateControls method.

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

Creating Controls


CreateControls method loops over IDs stored in the ControlIdList property, creates rows and adds them to placeholder exactly in the same order as they are stored in the ControlIdList.  (IDs in the list may be unordered because users are allowed to insert rows).

    private Hashtable m_controls = new Hashtable();

private void CreateControls() {
ArrayList ids = ControlIdList;
if (ids != null) {
foreach (int id in ids) {
WebUserControl control = CreateControl(id);
c_placeHolder.Controls.Add(control);
}
}
}

private WebUserControl CreateControl(int id) {
WebUserControl result =
(WebUserControl)LoadControl("WebUserControl.ascx");
result.ID = "c_" + id;
result.InsertAbove += InsertRowAbove;
result.InsertBellow += InsertRowBellow;
result.Remove += RemoveRow;
m_controls[id] = result;
return result;
}


References to created controls are saved for future use in the m_controls hash-table.  Saving references to dynamically created controls allows to avoid using FindControl method later.


Initialization


When a user first loads the page it has no ViewState and LoadViewState is not invoked.  However, the page should show the first row to user.  The first row can be created by the same CreateControls method, but you need to initialize ViewState backed properties before calling this method.  I did in the OnLoad method.  (You can do it in the OnInit method as well, but you cannot move exactly the same implementation to OnInit).

    protected override void OnLoad(EventArgs e) {
base.OnLoad(e);
if (!IsPostBack) {
LastControlId = 1;
ArrayList idList = new ArrayList();
idList.Add(0);
ControlIdList = idList;
CreateControls();
}
DisplayResult();
}

Note that initialization is only required when a user first opens the page.


Handling User Actions (Postback Events)


Server event fires when user clicks on any of the "Insert Above", "Insert Bellow" or "Remove".  It fires after the LoadViewState method is invoked, so dynamic controls are already reconstructed.  These controls have already loaded their previous ViewState.  TextBoxes and similar controls have already processed post-data and their Text properties reflect last changes made by the user.


A task of event handlers is to prepare the page for rendering reflecting changes requested by the user and to save these changes allowing correct reconstruction of the page after a postback.

    private void InsertRowBellow(object sender, EventArgs e) {
WebUserControl control = (WebUserControl)sender;
int index = control.Parent.Controls.IndexOf(control) + 1;
CreateControlAt(index);
DisplayResult();
}

private void CreateControlAt(int index) {
int id = GetNewId();
WebUserControl newControl = CreateControl(id);
ControlIdList.Insert(index, id);
c_placeHolder.Controls.AddAt(index, newControl);
}

private int GetNewId() {
int id = LastControlId++;
return id;
}

The CreateControlAt method does the most important job.  It generates new ID for the row which is unique across postbacks, creates new row controls and inserts it at the right position.  The CreateControlAt method also updates the ControlIdList property to allow correct reconstruction of the page on the next post-back. 


Using Dynamic Controls


The sample application displays concatenated text from all the text boxes below the rows.  The label text is updated in the OnLoad method. If the page layout is changed later in the event handler the label text is updated once again to reflect recent changes.

    private void DisplayResult() {
StringBuilder sb = new StringBuilder();
bool first = true;
foreach (int id in ControlIdList) {
if (!first) {
sb.Append(", ");
}
else {
first = false;
}
WebUserControl control = (WebUserControl)m_controls[id];
sb.Append(control.Text);
}
c_resultLabel.Text = sb.ToString();
}

Once Again



  1. Save enough information in view state to be able to reconstruct dynamically created controls.
  2. Override LoadViewState and reconstruct dynamically created controls using the the information retrieved from the view state.
  3. Initialize your page in the OnLoad method by setting initial values in the ViewState and then call your method to reconstruct controls.
  4. Create, remove or update your controls in response to user actions in event handlers.  Do not forget to update view state data used to reconstruct controls on the next postback.

7 comments:

Heath said...

I place a dropdown box in the ascx file and I can't get the value because it's private when I debug? How do I change this or get information on how to make this useful for getting dropdown values, checkboxes, etc.?

Yuriy Solodkyy said...

Add public properties to your user control. These properties should provide acess to your controls or their values. I am on vacation so I may be slow with answers.

Heath said...

Thanks Yuriy, I had finally figured it out today and was coming to post the answer. Have fun on vacation!

Add a function to the script section of the ascx file...like this to return the value:

public string DropDown
{
get
{
return c_DropDown.Text;
}
}

I wasn't thinking when I originally posted the question, but hopefully this will help someone else.

Heath said...

How would I put the code in a code behind page instead of on my aspx/cx page? I get errors on the lines using the WebUserControl on the aspx page. Thanks!

Unknown said...

Very informative post.
I did have problems downloading the code. It may be a broken link. FYI.
Thanks.

Yuriy Solodkyy said...

Thanks Leonardo. I indeed have problems with my uploads folder. Unfortunatelly I ocaasionally dropped this folder. I need to rebuild and ZIP all sample applications.

Anonymous said...

Hey, this is fantastic.
I could create a very good control out of this which my application required. Thanks a LOT for the post!!!