A coworker showed me this:
He has a DropDownList and a button on a web page. Here's the code behind:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ListItem item = new ListItem("1");
item.Attributes.Add("title", "A");
ListItem item2 = new ListItem("2");
item2.Attributes.Add("title", "B");
DropDownList1.Items.AddRange(new[] {item, item2});
string s = DropDownList1.Items[0].Attributes["title"];
}
}
protected void Button1_Click(object sender, EventArgs e)
{
DropDownList1.Visible = !DropDownList1.Visible;
}
On the page load, the items' tooltips are showing, but on the first postback, the attributes are lost. Why is this the case, and are there any workarounds?
I had the same problem and wanted to contribute this resource where the author created an inherited ListItem Consumer to persist attributes to ViewState. Hopefully it will save someone the time I wasted until I stumbled on it.
protected override object SaveViewState()
{
// create object array for Item count + 1
object[] allStates = new object[this.Items.Count + 1];
// the +1 is to hold the base info
object baseState = base.SaveViewState();
allStates[0] = baseState;
Int32 i = 1;
// now loop through and save each Style attribute for the List
foreach (ListItem li in this.Items)
{
Int32 j = 0;
string[][] attributes = new string[li.Attributes.Count][];
foreach (string attribute in li.Attributes.Keys)
{
attributes[j++] = new string[] {attribute, li.Attributes[attribute]};
}
allStates[i++] = attributes;
}
return allStates;
}
protected override void LoadViewState(object savedState)
{
if (savedState != null)
{
object[] myState = (object[])savedState;
// restore base first
if (myState[0] != null)
base.LoadViewState(myState[0]);
Int32 i = 1;
foreach (ListItem li in this.Items)
{
// loop through and restore each style attribute
foreach (string[] attribute in (string[][])myState[i++])
{
li.Attributes[attribute[0]] = attribute[1];
}
}
}
}
Thanks, Laramie. Just what I was looking for. It keeps the attributes perfectly.
To expand, below is a class file I created using Laramie's code to create a dropdownlist in VS2008. Create the class in the App_Code folder. After you create the class, use this line on the aspx page to register it:
<%# Register TagPrefix="aspNewControls" Namespace="NewControls"%>
You can then put the control on your webform with this
<aspNewControls:NewDropDownList ID="ddlWhatever" runat="server">
</aspNewControls:NewDropDownList>
Ok, here's the class...
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Security.Permissions;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace NewControls
{
[DefaultProperty("Text")]
[ToolboxData("<{0}:ServerControl1 runat=server></{0}:ServerControl1>")]
public class NewDropDownList : DropDownList
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
protected override object SaveViewState()
{
// create object array for Item count + 1
object[] allStates = new object[this.Items.Count + 1];
// the +1 is to hold the base info
object baseState = base.SaveViewState();
allStates[0] = baseState;
Int32 i = 1;
// now loop through and save each Style attribute for the List
foreach (ListItem li in this.Items)
{
Int32 j = 0;
string[][] attributes = new string[li.Attributes.Count][];
foreach (string attribute in li.Attributes.Keys)
{
attributes[j++] = new string[] { attribute, li.Attributes[attribute] };
}
allStates[i++] = attributes;
}
return allStates;
}
protected override void LoadViewState(object savedState)
{
if (savedState != null)
{
object[] myState = (object[])savedState;
// restore base first
if (myState[0] != null)
base.LoadViewState(myState[0]);
Int32 i = 1;
foreach (ListItem li in this.Items)
{
// loop through and restore each style attribute
foreach (string[] attribute in (string[][])myState[i++])
{
li.Attributes[attribute[0]] = attribute[1];
}
}
}
}
}
}
Simple solution is to add the tooltip attributes in the pre-render event of the dropdown. Any changes to the state should be done at pre-render event.
sample code :
protected void drpBrand_PreRender(object sender, EventArgs e)
{
foreach (ListItem _listItem in drpBrand.Items)
{
_listItem.Attributes.Add("title", _listItem.Text);
}
drpBrand.Attributes.Add("onmouseover", "this.title=this.options[this.selectedIndex].title");
}
If you only want to load the listitems on the first load of the page then you will need to enable ViewState so that the control can serialize its state there and reload it when the page posts back.
There are several places where ViewState can be enabled - check the <pages/> node in the web.config and also in the <%# page %> directive at the top of the aspx file itself for the EnableViewState property. This setting will need to be true for ViewState to work.
If you don't want to use ViewState, simply remove the if (!IsPostBack) { ... } from around the code that adds the ListItems and the items will be recreated on each postback.
Edit: I apologize - I misread your question. You are correct that the attributes do no survive postback as they are not serialized in ViewState. You must re-add those attributes on each postback.
One simple solution- Call your drop down loading function on the click event where you request for post back.
Here's the VB.Net code of the solution proposed by Laramie and refined by gleapman.
Update: The code I posted below is actually for the ListBox control. Just change the inheritance to DropDownList and rename the class.
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Security.Permissions
Imports System.Linq
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Namespace CustomControls
<DefaultProperty("Text")> _
<ToolboxData("<{0}:ServerControl1 runat=server></{0}:ServerControl1>")>
Public Class PersistentListBox
Inherits ListBox
<Bindable(True)> _
<Category("Appearance")> _
<DefaultValue("")> _
<Localizable(True)> _
Protected Overrides Function SaveViewState() As Object
' Create object array for Item count + 1
Dim allStates As Object() = New Object(Me.Items.Count + 1) {}
' The +1 is to hold the base info
Dim baseState As Object = MyBase.SaveViewState()
allStates(0) = baseState
Dim i As Int32 = 1
' Now loop through and save each attribute for the List
For Each li As ListItem In Me.Items
Dim j As Int32 = 0
Dim attributes As String()() = New String(li.Attributes.Count - 1)() {}
For Each attribute As String In li.Attributes.Keys
attributes(j) = New String() {attribute, li.Attributes(attribute)}
j += 1
Next
allStates(i) = attributes
i += 1
Next
Return allStates
End Function
Protected Overrides Sub LoadViewState(savedState As Object)
If savedState IsNot Nothing Then
Dim myState As Object() = DirectCast(savedState, Object())
' Restore base first
If myState(0) IsNot Nothing Then
MyBase.LoadViewState(myState(0))
End If
Dim i As Int32 = 0
For Each li As ListItem In Me.Items
' Loop through and restore each attribute
' NOTE: Ignore the first item as that is the base state and is represented by a Triplet struct
i += 1
For Each attribute As String() In DirectCast(myState(i), String()())
li.Attributes(attribute(0)) = attribute(1)
Next
Next
End If
End Sub
End Class
End Namespace
Typical solutions to this problem involves creating new controls that are not quite feasible in normal circumstances. There is a simple yet trivial solution to this problem.
The issue is that the ListItem loses its attributes on postback. However, the List itself never loses any custom attributes. One can take advantage of this in a simple yet effective manner thus.
Steps:
Serialize your attributes using the code in the answer above (https://stackoverflow.com/a/3099755/3624833)
Store it to a custom attribute of the ListControl (dropdownlist, checklistbox, whatever).
On post back, read back the custom attribute from the ListControl and then deserialize it back as attributes.
Here is the code I used to (de)serialize attributes (What I needed to do was to keep track of what items of the list were originally rendered as selected when retrieved from the backend and then save or delete rows as per the changes made by the user on the UI):
string[] selections = new string[Users.Items.Count];
for(int i = 0; i < Users.Items.Count; i++)
{
selections[i] = string.Format("{0};{1}", Users.Items[i].Value, Users.Items[i].Selected);
}
Users.Attributes["data-item-previous-states"] = string.Join("|", selections);
(above, "Users" is a CheckboxList control).
On post back (in my case a Submit button Click event), I use the below code to retrieve the same and store them into a Dictionary for post processing:
Dictionary<Guid, bool> previousStates = new Dictionary<Guid, bool>();
string[] state = Users.Attributes["data-item-previous-states"].Split(new char[] {'|'}, StringSplitOptions.RemoveEmptyEntries);
foreach(string obj in state)
{
string[] kv = obj.Split(new char[] { ';' }, StringSplitOptions.None);
previousStates.Add(kv[0], kv[1]);
}
(PS: I have a library funcs that perform error handling and data conversions, omitting the same here for brevity).
Simple solution without ViewState, creating new server control or smth complex:
Creating:
public void AddItemList(DropDownList list, string text, string value, string group = null, string type = null)
{
var item = new ListItem(text, value);
if (!string.IsNullOrEmpty(group))
{
if (string.IsNullOrEmpty(type)) type = "group";
item.Attributes["data-" + type] = group;
}
list.Items.Add(item);
}
Updating:
public void ChangeItemList(DropDownList list, string eq, string group = null, string type = null)
{
var listItem = list.Items.Cast<ListItem>().First(item => item.Value == eq);
if (!string.IsNullOrEmpty(group))
{
if (string.IsNullOrEmpty(type)) type = "group";
listItem.Attributes["data-" + type] = group;
}
}
Example:
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
using (var context = new WOContext())
{
context.Report_Types.ToList().ForEach(types => AddItemList(DropDownList1, types.Name, types.ID.ToString(), types.ReportBaseTypes.Name));
DropDownList1.DataBind();
}
}
else
{
using (var context = new WOContext())
{
context.Report_Types.ToList().ForEach(types => ChangeItemList(DropDownList1, types.ID.ToString(), types.ReportBaseTypes.Name));
}
}
}
#Sujay
You could add a semi-colon separated text into the dropdown's value attribute (like csv style), and use String.Split(';') to get 2 "values" out of the one value, as a workaround to get away with not having to create anew user control. Especially if you only have few extra attributes, and if it is not too long. You could also use a JSON value into the dropdown's value attribute and then parse out whatever you need from there.
//In the same block where the ddl is loaded (assuming the dataview is retrieved whether postback or not), search for the listitem and re-apply the attribute
if(IsPostBack)
foreach (DataRow dr in dvFacility.Table.Rows)
{
//search the listitem
ListItem li = ddl_FacilityFilter.Items.FindByValue(dr["FACILITY_CD"].ToString());
if (li!=null)
{
li.Attributes.Add("Title", dr["Facility_Description"].ToString());
}
} //end for each
I managed to achieve that using Session Variables, in my case my list is not going to contain many elements so it works pretty well, this is how I did it:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
string[] elems;//Array with values to add to the list
for (int q = 0; q < elems.Length; q++)
{
ListItem li = new ListItem() { Value = "text", Text = "text" };
li.Attributes["data-image"] = elems[q];
myList.Items.Add(li);
HttpContext.Current.Session.Add("attr" + q, elems[q]);
}
}
else
{
for (int o = 0; o < webmenu.Items.Count; o++)
{
myList.Items[o].Attributes["data-image"] = HttpContext.Current.Session["attr" + o].ToString();
}
}
}
When the Page is loaded first time the list is populated and I add an Image attribute which is lost after postback :( so at the time I add the elements with its attributes I create one Session variable "attr" plus the number of the element taken from the "for" cycle (it will be like attr0, attr1, attr2, etc...) and in them I save the value of the attribute (a path to an image in my case), when postback occurs (inside the "else") I just loop the list and add the attribute taken from the Session variable using the "int" of the "for" loop that is the same as when the page was loaded (this is because in this page I do not add elements to the list just selecting so they have always the same index) and the attributes are set again, I hope this helps someone in the future, greetings!
Related
In my project I've got a variable numtorust which represents the number of the toursits.
(for testing purposes numtourist = 2)
for (i=0,numtoursut; i++)
I create dynamically 5 checkboxes with assigned checkedChanged event for each of tourist. Also to keep track which checkbox to which tourist applies I add attribute 'collection'
mycheckbox.InputAttributes.Add("collection", i.ToString());
In checkedchanged event handler - when a user selects a checkbox I checked if its collection attribute is = 0 or 1 (first or second user). then I add checkbox value to myche1 which is of type List<string) if collection attribute = 1.
But I when I decided to make one array of type List<string> with name Toursit
when I try to add an element to it I got an exception - Object reference not set to an instance of object in this row of my code
Toursist[Int32.Parse(chk.InputAttributes["collection"])].Add(chk.InputAttributes["value"].ToString());
Here is my full code
protected void checkChanged(object sender, EventArgs e)
{
CheckBox chk = (CheckBox)sender;
/*that doesn't work
if (chk.Checked)
{
Toursist[Int32.Parse(chk.InputAttributes["collection"])].Add(chk.InputAttributes["value"].ToString());
((List<String>[])Session["chk"])[Int32.Parse(chk.InputAttributes["collection"])] = Toursist[Int32.Parse(chk.InputAttributes["collection"])];
}*/
//this works with myche1 of type list<string>
if ((chk.Checked)&&(chk.InputAttributes["collection"].Equals("1")))
{
myche1.Add(chk.InputAttributes["value"].ToString());
lblProba.Text += chk.InputAttributes["value"].ToString();
Session["chk1"] = myche1;
}
}
edit 1:
teh new code of
protected void checkChanged(object sender, EventArgs e)
{
List<string>[] Toursist = new List<string>[2];
//Session["chk"] = new List<string>[2];
for (int i = 0; i < Toursist.Length; i++)
{
Toursist[i] = new List<string>();
// ((List<String>[])Session["chk"])[i] = Toursist[i];
}
CheckBox chk = (CheckBox)sender;
if (chk.Checked)
{
if (((List<String>[])Session["chk"])[Int32.Parse(chk.InputAttributes["collection"])] == null)
{
((List<String>[])Session["chk"])[Int32.Parse(chk.InputAttributes["collection"])] = Toursist[Int32.Parse(chk.InputAttributes["collection"])];
}
Toursist[Int32.Parse(chk.InputAttributes["collection"])].Add(chk.InputAttributes["value"].ToString());
lblProba.Text += chk.InputAttributes["collection"].ToString();
((List<String>[])Session["chk"])[Int32.Parse(chk.InputAttributes["collection"])] = Toursist[Int32.Parse(chk.InputAttributes["collection"])];
}
again the same mistake this time when I test if Sessio["chk"] == 0.
but If I uncomment (so I no more have this mistake)
// ((List<String>[])Session["chk"])[i] = Toursist[i];
on each postback event my session will be empty and I don't want!!
You haven't created any lists. When you create an array of lists, it won't automatically create all the lists in the array, you have to do that manually:
List<string>[] Toursist = new List<string>[numtoursut];
for (int i = 0; i < Toursist.Length; i++) {
Toursist[i] = new List<string>();
}
i have a gridview in which i am using checkbox in each row. i am trying to access checkbox of each row and trying to find out which checkboxes have been checked.buut when i try to run the below code.the condition always stands to be false and the inner if condition is never reached by the code.kindly help me.thanks in advance.
protected void btn_3id_Click(object sender, EventArgs e)
{
string str = "";
string srr = "";
for (int i = 0; i < GridView1.Rows.Count;i++ )
{
CheckBox chk = (CheckBox)GridView1.Rows[i].FindControl("CheckBox1");
if (chk.Checked==true)
{
if (str == "")
{
str = GridView1.Rows[i].Cells[0].Text.ToString();
}
else
{
srr = str + "," + GridView1.Rows[i].Cells[0].Text.ToString();
}
}
}
Session["Card_id"] = str;
Response.Redirect("ID.aspx");
}
The code looks fine.
The problem could be you are binding the gridview at page load.
Try grid binding in the following section of page load
if(!Page.IsPostBack)
{
//code to bind the gridview
}
I can only guess that you are binding your gridview on each page load without checking PostBack. That is causing the checkbox to loose its current state. So where you are assigning the DataSource to the Gridview , Check for PostBack like:
if(!Page.IsPostBack)
{
GridView1.DataSource = yourDataSource;
GridView1.DataBind();
}
also you can do some minor improvements in your code like your check:
if(chk.Checked == true)
can be replaced as:
if(chk.Checked) //Since it returns a bool value.
You can omit multiple string variables for concatenation. Its better if you use StringBuilder, (See why it is better) so your code would be:
protected void btn_3id_Click(object sender, EventArgs e)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < GridView1.Rows.Count;i++ )
{
CheckBox chk = (CheckBox)GridView1.Rows[i].FindControl("CheckBox1");
if (chk.Checked==true)
{
sb.Append() GridView1.Rows[i].Cells[0].Text.ToString();
}
}
Session["Card_id"] = sb.ToString();
Response.Redirect("ID.aspx");
}
if(!Page.IsPostBack)
{
//
}
Postback plays important role in cs file. If you are clearing values on page load , you will null values of checkbox.
You code is fine.
Just try to do this...
am creating some TextBoxes by backend on a text change event, Like this :
protected void txtHowMany_TextChanged(object sender, EventArgs e)
{
int totalSections = Convert.ToInt32(txtHowMany.Text.Trim());
for (int i = 1; i <= totalSections; i++)
{
TextBox tbx = new TextBox();
tbx.Text = "";
tbx.ID = "section" + i;
tbx.Style.Add("width", "90%");
tdSectionsAdd.Controls.Add(tbx);
}
trSectionsName.Visible = true;
}
The auto post back is true for txtHowMany, so when I enter a number, it generates the textboxes and add it to table division
Now the problem is, I am trying to get text from generated textboxes like this :
protected void btnSave_click(object sender, EventArgs e)
{
int numbersOfSectionsToSave = 1;
int sectionsToSave =Convert.ToInt32(txtHowMany.Text.Trim());
for (int i = 1; i < sectionsToSave; i++)
{
Sections section = new Sections();
section.CourseId = result;
section.OrganizationId = course.OrganizationId;
foreach (Control c in tdSectionsAdd.Controls)
{
if (c.GetType() == typeof(TextBox))
{
TextBox txtBox = (TextBox)c;
string id = "section" + i;
if (txtBox.ID == id)
{
section.Name = txtBox.Text.Trim();
}
}
}
string name = Request.Form["section1"];
section.CreatedBy = "Admin";
section.CreationDate = DateTime.Now;
section.ModifiedBy = "Admin";
section.ModificationDate = DateTime.Now;
numbersOfSectionsToSave += section.SaveSection();
}
But its showing 0 count for the controls in tdSectionsAdd , The controls are added before I am trying to access them, but still it shows no controls in td.
Please help, How can I get these textboxes?
Thanks!
You need to add them in each postback. Store the totalSections variable in ViewState so you can add them i page load also:
protected void AddTextBoxes()
{
int totalSections;
if (int.TryParse(Convert.ToString(ViewState["TotalSections"]), out totalSections)
{
for (int i = 1; i <= totalSections; i++)
{
TextBox tbx = new TextBox();
tbx.Text = "";
tbx.ID = "section" + i;
tbx.Style.Add("width", "90%");
tdSectionsAdd.Controls.Add(tbx);
}
trSectionsName.Visible = true;
}
}
protected void txtHowMany_TextChanged(object sender, EventArgs e)
{
ViewState["TotalSections"] = Convert.ToInt32(txtHowMany.Text.Trim());
tdSectionsAdd.Controls.Clear();
AddTextBoxes();
}
protected void Page_Load(object sender, EventArgs e)
{
AddTextBoxes();
}
Dynamic Created controls "Disappear" on postback, if they are not "recreated" in the Page_Init of that page.
Only if they are created in the page_init will the page's viewstate get updated with their information.
Long Explantion:
When we perform a postback (or partial postback) we want to be able to access those controls (or at least the values the user put into them).
We know that the data is in the viewstate, but ASP.NET doesn’t really know which control a ViewState item belongs to. It only knows to match a viewstate item and a control through the same index (e.g. Matches item n in the viewstate tree to item n in the control tree). Therefore in order to get the dynamic controls' data, we need to re-create the controls each time the page is postbacked.
BUT in order for this to work, we need to re-create the controls in the Page_Init function NOT in the Page_Load.
Why? Because when the ViewState is created it needs all the controls to already exist.
This is taken from MSDN, as you can see the viewstate is loaded AFTER the init but before the page load.
TL;DR Call the function that creates the dynamic controls in the page_init and you should be able to see all the values the user entered when the page postbacks
A few links on this issue:
http://forums.asp.net/t/1186195.aspx/1
ASP.NET - Dynamic controls created in Page_Pre_init() or Page_Init() or Page_Load()
Option 2:
I should note: If the controls all had unique Ids and you're not interested in re-creating them again every postback - you could always look for them in the Request Object.
The Request.Form is a NameValueCollection that holds the values of all the controls that were part of the form, just search it for whatever you're looking for
I have a list of strings, which are generated on a imagebutton_click method. I want to be able to use this list in another webpage.
How ever im not quite sure how to go about posting it between the two pages.
I have the following code below:
protected void ImageButton1_Click(object sender, ImageClickEventArgs e)
{
RadGrid rg = RadGrid1;
//Get selected rows
GridItemCollection gdc = (GridItemCollection)rg.SelectedItems;
foreach (GridItem gi in gdc)
{
if (gi is GridDataItem)
{
GridDataItem gdi = (GridDataItem)gi;
if (!string.IsNullOrEmpty(gdi["Email"].Text))
{
string client = gdi["Email"].Text;
//Creating a List of Clients to be Emailed
emailList.Add(email);
}
}
//Enable the Prepare Email Page
PageView2.Selected = true;
}
protected void ImageButton2_Click(object sender, ImageClickEventArgs e)
{
if (emailList.Count != 0)
{
for (int i = 0; i < emailList.Count; i++)
{
_to = emailList[i].ToString() + ";";
}
}
else
{
_to = emailList[1].ToString();
}
//Processing Client Email
string _from = sec.GetCurrentUserEmail("test");
string _cc = "";
string _subject = SubjectTB.Text;
string _body = EmailEditor.Content;
string _tempTo = sec.GetCurrentUserEmail("temp");
string _msg = sec.SendMail(_tempTo, _cc, _from, _subject, _body, "");
if (_msg == "success")
{
//Thank the user and record mail was delivered sucessfully
TestPanel.Visible = true;
}
}
How do I get the values of emailList to be passed through to ImageButton2_click(object sender, ImageClickEventArgs e). Currently it just passes through a null value. I gather I need to use HTML forms to do the request. Thanks.
I'm guessing that emailList is a private variable? Wouldn't you be able to add that to the LoadControlState and SaveControlState so that it'll be available for ImageButton2_Click later?
Here is an example: http://msdn.microsoft.com/en-us/library/system.web.ui.control.loadcontrolstate%28v=vs.80%29.aspx
Another possibility is hidden field, that might be the simplist way, but not as secure.
You can get a good idea about state management in ASP.Net here. For your case if button1 and button2 are in the same aspx page, Viewstate would be a good idea. If they are in diferent pages then use Session state management.
I have the following code:
public partial class queryTerm : System.Web.UI.UserControl
{
private static readonly List<string> BooleanOperators = new List<string> { ".", "AND", "AND NOT", "OR", "OR NOT" };
protected void BuildBoolPanel()
{
var parensOpen = _labelBoolean.Text;
foreach (var #operator in BooleanOperators)
{
if (parensOpen == #operator)
{
continue;
}
var linkButton = new LinkButton();
linkButton.Text = #operator;
linkButton.CommandArgument = #operator;
linkButton.CommandName = "parensOpen";
linkButton.Command += new CommandEventHandler(linkButton_Command);
_popupMenuParensOpen.Controls.Add(linkButton);
var literalLineBreak = new Literal();
literalLineBreak.Text = "<BR/>";
_popupMenuParensOpen.Controls.Add(literalLineBreak);
}
}
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
BuildBoolPanel();
}
void linkButton_Command(object sender, CommandEventArgs e)
{
_labelBoolean.Text = (string)e.CommandArgument;
BuildBoolPanel();
}
}
I have a panel(it's _popupMenuParensOpen) that is shown with the hoverextender whenever the cursor finds itself over a specific label in my user control.
This panel has all the boolean operators and '.' meaning not set.
I programatically add the boolean operators as a label in my panel, and I only add those that don't match what it is currently set to. For instance if my label is set to 'AND', when I hover over it, I display everything but 'AND'.
The problem is these never call linkButton_Command even though I instruct them to.
Weirder yet, if I remove the 'if (!this.IsPostBack) in page load, it will call it.
My control is inside an updatePanel.
The issue is that you're dynamically adding the controls during the Page_Load event. When a postback occurs those controls aren't going to exist and will need to be created every time. This is why when you remove your if (!Page.IsPostBack), and the controls are rebuilt, it works.
Since you're building this as a user control, you might want to look in to overriding the CreateChildControls method.