I'm having some difficulty with my dynamically generated usercontrols retaining their selected values. I have created a usercontrol with several form fields. On the main page I have a button that will place my usercontrol in a placeholder. The user can create as many of these as needed. I also have a button in the control itself that allows the user to delete any given control. This functionality seems to be working fine.
But, weirdness ensues in the following scenario:
Click button to create a usercontrol on page. Fill out the form
fields.
Click button again to create a second UC and fill out form
fields. All is well at this point. Values in UC#1 retained its
values.
Click button to create third UC and all selected values are
wiped out of UC #1 and #2.
If I refill out the fields in all the UC's then click the button to create a 4th UC, then UC#1 and UC#3 retain their values but UC#2 loses it's values.
Any help would be massively appreciated. I'm going bug-eyed trying to figure this one out. This is my first foray into dynamic usercontrols and so far it's kicking my butt. And I still have to figure out how to populate these UC's with values from the db so the user can come back and edit the form, but one thing at a time.
aspx:
<asp:PlaceHolder ID="placeholderOffenseCodes" runat="server"> </asp:PlaceHolder>
<asp:Button ID="btnAddOffense" runat="server" Text="Add an Offense" CausesValidation="false" OnClick="btnAddOffense_Click" />
<!--The text value determines how many items are initially displayed on the page-->
<asp:Literal ID="ltlCount" runat="server" Text="0" Visible="false" />
<asp:Literal ID="ltlRemoved" runat="server" Visible="false" />
aspx.cs:
protected void Page_Load(object sender, EventArgs e)
{
AddAndRemoveDynamicOffenseControls();
}
private void AddAndRemoveDynamicOffenseControls()
{
//Determine which control fired the postback event.
Control c = GetPostBackOffenseControl(Page);
if ((c != null))
{
//If the add button was clicked, increase
//the count to let the page know we want
//to display an additional user control
if (c.ID.ToString() == "btnAddOffense")
{
ltlCount.Text = Convert.ToString(Convert.ToInt16(ltlCount.Text) + 1);
}
}
//Be sure everything in the placeholder control is cleared out
placeholderOffenseCodes.Controls.Clear();
int ControlID = 0;
//Re-add controls every time the page loads.
for (int i = 0; i <= (Convert.ToInt16(ltlCount.Text) - 1); i++)
{
IncidentGroupA_Offenses uc = (IncidentGroupA_Offenses)LoadControl("IncidentGroupA_Offenses.ascx");
//If this particular control id has been deleted
//from the page, DO NOT use it again. If we do, it will
//pick up the viewstate data from the old item that
//had this control id, instead of generating
//a completely new control. Instead, increment
//the control ID so we're guaranteed to get a "new"
//control that doesn't have any lingering information in the viewstate.
while (InDeletedOffenseList("offense" + ControlID) == true)
{
ControlID += 1;
}
//Note that if the item has not been deleted from the page,
//we DO want it to use the same control id
//as it used before, so it will automatically maintain
//the viewstate information of the user control
//for us.
uc.ID = "offense" + ControlID;
//Add an event handler to this control to raise
//an event when the delete button is clicked
//on the user control
uc.RemoveOffenseUC += this.HandleRemoveOffenseUserControl;
//Add the user control to the panel
placeholderOffenseCodes.Controls.Add(uc);
//Add Offense number to label on usercontrol
int OffenseNum = i + 1;
uc.OffenseNumber = "Offense " + OffenseNum;
//Increment the control id for the next round through the loop
ControlID += 1;
}
}
protected void btnAddOffense_Click(object sender, EventArgs e)
{
//handled in page_load
}
private bool InDeletedOffenseList(string ControlID)
{
//Determine if the passed in user control ID
//has been stored in the list of controls that
//were previously deleted off the page
string listvalues = ltlRemoved.Text;
string[] stringSeparators = new string[] { "|" };
string[] DeletedList = listvalues.Split(stringSeparators, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i <= DeletedList.GetLength(0) - 1; i++)
{
if (ControlID == DeletedList[i])
{
return true;
}
}
return false;
}
public void HandleRemoveOffenseUserControl(object sender, EventArgs e)
{
//This handles delete event fired from the user control
//Get the user control that fired this event, and remove it
LinkButton linkBtn = sender as LinkButton;
IncidentGroupA_Offenses uc = (IncidentGroupA_Offenses)linkBtn.Parent;
if (uc != null)
{
placeholderOffenseCodes.Controls.Remove(uc);
}
//Keep a pipe delimited list of which user controls were removed. This will increase the
//viewstate size if the user keeps removing dynamic controls, but under normal use
//this is such a small increase in size that it shouldn't be an issue.
ltlRemoved.Text += uc.ID.ToString() + "|";
//Also, now that we've removed a user control decrement the count of total user controls on the page
ltlCount.Text = Convert.ToString(Convert.ToInt16(ltlCount.Text) - 1);
}
public Control GetPostBackOffenseControl(Page page)
{
Control control = null;
string ctrlname = page.Request.Params.Get("__EVENTTARGET");
if ((ctrlname != null) & ctrlname != string.Empty)
{
control = page.FindControl(ctrlname);
}
else
{
foreach (string ctl in page.Request.Form)
{
Control c = page.FindControl(ctl);
if (c is System.Web.UI.WebControls.Button)
{
control = c;
break;
}
}
}
return control;
}
.ascx.cs:
public event EventHandler RemoveOffenseUC;
protected void btnRemoveOffense_Click(object sender, EventArgs e)
{
//Raise this event so the parent page can handle it
if (RemoveOffenseUC != null)
{
RemoveOffenseUC(sender, e);
}
}
public string OffenseNumber
{
get { return lblOffenseNumber.Text; }
set { lblOffenseNumber.Text = value; }
}
I always thought you had to add dynamic controls at Page_Init to ensure correct loading of ViewState. Maybe that simple change would fix your problem?
Otherwise, I've had luck in the past with avoiding dynamic controls altogether using a repeater. Instead of adding dynamic controls, which webforms is definitely not very good at, add data to a data structure like a List or ADO DataTable, then bind that to an asp:Repeater with the controls you want in it. No messy fussing around with dynamic controls.
Best of luck!
Related
I have an .aspx page and I want to dynamically add textboxes to the page on the click of a button. For this purpose I've added a placeholder on the page and add the controls to the server side when the button is clicked.
<asp:PlaceHolder runat="server" ID="NotificationArea"></asp:PlaceHolder>
<asp:Button ID="AddNotification" runat="server" Text="Add" OnClick="AddNotification_Click" />
<asp:Button ID="RemoveNotification" runat="server" Text="Remove" OnClick="RemoveNotification_Click" />
I store the textboxes in a session variable so that I can continue adding and removing textboxes indefinitely. Below I put the on_click methods for the add and remove buttons:
protected void AddNotification_Click(object sender, EventArgs e)
{
List<TextBox> notifications = (List<TextBox>)(Session["Notifications"]);
notifications.Add(new TextBox());
notifications[notifications.Count - 1].Width = 450;
notifications[notifications.Count - 1].ID = "txtNotification" + notifications.Count;
foreach (TextBox textBox in notifications)
{
NotificationArea.Controls.Add(textBox);
}
NotificationArea.Controls.Add(notifications[notifications.Count - 1]);
Session["Notifications"] = notifications;
}
protected void RemoveNotification_Click(object sender, EventArgs e)
{
List<TextBox> notifications = (List<TextBox>)(Session["Notifications"]);
if (notifications.Count > 0)
{
NotificationArea.Controls.Remove(notifications[notifications.Count - 1]);
notifications.RemoveAt(notifications.Count - 1);
}
foreach (TextBox textBox in notifications)
{
NotificationArea.Controls.Add(textBox);
}
Session["Notifications"] = notifications;
}
This works nicely. It continually adds new textboxes and removes the last one if the remove button is clicked. Then, when I tried to get the text from the textboxes I ran into a problem. I never actually store the text that are typed into the textboces in the session variable. Just the empty textboxes that are initially created. Also, see below:
int count = NotificationArea.Controls.Count;
Debugging this shows that the count of the controls in the NotificationArea is 0. How do I access the text for these dynamically added textbox controls? Do I somehow add an ontext_change event to the textboxes that saves the particular textbox's Text into its equivalent in the session variable? How do I go about doing that?
Found the solution here. Turns out you need to re-create all controls that were added dynamically on each post back
public void Page_Init(object sender, EventArgs e)
{
CreateDynamicControls();
}
private void CreateDynamicControls()
{
notifications = (List<TextBox>)(Session["Notifications"]);
if (notifications != null)
{
foreach (TextBox textBox in notifications)
{
NotificationArea.Controls.Add(textBox);
}
}
}
Doing this allows me to access the content of these controls at any time.
I am working on a page that accepts scanned input into a textbox. Once input is entered into the textbox, a user control that contains additional fields for the input item is dynamically generated via autopostback. The user control also contains a link button that is intended to delete the item (itself). Clicking this button generates a "delete" event that is supposed to bubble up to the parent.
Unfortunately this does not seem to happen - the deletion handling code is never reached. My understanding is that it is because the controls have not been generated yet at page load, so the event handlers have not been created yet. But since I don't know what controls need to be generated at page load (since the user input hasn't been processed yet), I can't move the code to generate the user controls to PageLoad.
How should this be handled? Am I going about this the wrong way? Most of my relevant code is below.
Thanks for any help!
page.aspx:
Enter SKU (tab to enter):
<asp:TextBox ID="EntryTextBox" CssClass="textbox" AutoPostBack="true" OnTextChanged="newItem" runat="server"></asp:TextBox>
<asp:Panel ID="itempanel" runat="server">
</asp:Panel>
page.aspx.cs:
protected void newItem(object sender, EventArgs e)
{
// we need to store item entries in ViewState so they comeback on postback;
// they can't be stored in the controls themselves as the controls will
// disappear
ViewState["skus"] += "\t" + EntryTextBox.Text;
ViewState["descs"] += "\t" + itemLookup(EntryTextBox.Text);
// ...more item descriptors...
updateItemPanel();
}
protected void updateItemPanel()
{
// generate a control for each item entered in ViewState
itempanel.Controls.Clear();
List<string> skus = new List<string>(ViewState["items"].ToString().Substring(1).Split('\t'));
List<string> descs = new List<string>(ViewState["descs"].ToString().Substring(1).Split('\t'));
// ...more item descriptors...
int i = 0;
foreach (string sku in skus)
{
item newitemctrl = (item)Page.LoadControl("~/item.ascx");
newitemctrl.line = (i + 1).ToString();
newitemctrl.sku = skus[i];
newitemctrl.description = descs[i];
// ...more item descriptors...
newitemctrl.deleteLinkClicked += new EventHandler(deleteClicked);
itempanel.Controls.Add(newitemctrl);
i++;
}
}
protected void deleteClicked(object sender, EventArgs e)
{
List<string> skus = new List<string>(ViewState["skus"].ToString().Substring(1).Split('\t'));
List<string> descs = new List<string>(ViewState["descs"].ToString().Substring(1).Split('\t'));
// ...more item descriptors...
item olditemctrl = (item)sender;
skus.RemoveAt(Convert.ToInt32(olditemctrl.number) - 1);
descs.RemoveAt(Convert.ToInt32(olditemctrl.number) - 1);
ViewState["skus"] = skus.ToString();
ViewState["descs"] = descs.ToString();
updateItemPanel();
}
item.ascx:
<asp:LinkButton ID="DeleteLinkButton" runat="server" onclick="DeleteLinkButton_Click">Delete</asp:LinkButton>
item.ascx.cs:
public event EventHandler deleteLinkClicked;
protected void DeleteLinkButton_Click(object sender, EventArgs e)
{
if (this.deleteLinkClicked != null)
{
this.deleteLinkClicked(new object(), new EventArgs());
}
}
You can dispatch a postback event in javascript by adding this as onclick event:
__doPostBack("<%= button.ClientID %>", "");
DoPostBack has two arguments, the first is the ID, the second is the event name.
I used this solution successfully.
You'll find more information here.
Note:
This itself does not fire the event automatically, but you can see what you want to happen in the Load Event of your page.
You can get the arguments like this: : Request.Form["__EVENTTARGET"]
You have full access to the form data, so you can also get the values from the dynamically created controls
I am dynamically adding a custom user control to an update panel. My user control contains two dropdownlists and a textbox. When a control outside of the update panel triggers a postsback, I am re-adding the user control to the update panel.
The problem is...on postback when I re-add the user controls, it's firing the "SelectedIndexChanged" event of the dropdownlists inside the user control. Even if the selectedindex did not change since the last postback.
Any ideas?
I can post the code if necessary, but there's quite a bit in this particular scenario.
Thanks in advance!
EDIT...CODE ADDED BELOW
*.ASCX
<asp:DropDownList ID="ddlColumns" OnSelectedIndexChanged="ddlColumns_SelectedChanged" AppendDataBoundItems="true" AutoPostBack="true" runat="server">
*.ASCX.CS
List<dataColumnSpecs> dataColumns = new List<dataColumnSpecs>();
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
fillDDLColumns();
}
}
public void fillDataColumnsList()
{
dataColumns.Clear();
//COMMON GETDATATABLE RETURNS A DATA TABLE POPULATED WITH THE RESULTS FROM THE STORED PROC COMMAND
DataTable dt = common.getDataTable(storedProcs.SELECT_COLUMNS, new List<SqlParameter>());
foreach (DataRow dr in dt.Rows)
{
dataColumns.Add(new dataColumnSpecs(dr["columnName"].ToString(), dr["friendlyName"].ToString(), dr["dataType"].ToString(), (int)dr["dataSize"]));
}
}
public void fillDDLColumns()
{
fillDataColumnsList();
ddlColumns.Items.Clear();
foreach (dataColumnSpecs dcs in dataColumns)
{
ListItem li = new ListItem();
li.Text = dcs.friendlyName;
li.Value = dcs.columnName;
ddlColumns.Items.Add(li);
}
ddlColumns.Items.Insert(0, new ListItem(" -SELECT A COLUMN- ", ""));
ddlColumns.DataBind();
}
protected void ddlColumns_SelectedChanged(object sender, EventArgs e)
{
//THIS CODE IS BEING FIRED WHEN A BUTTON ON THE PARENT *.ASPX IS CLICKED
}
*.ASPX
<asp:UpdatePanel ID="upControls" runat="server">
<ContentTemplate>
<asp:Button ID="btnAddControl" runat="server" Text="+" OnClick="btnAddControl_Click" />
</ContentTemplate>
</asp:UpdatePanel>
<asp:Button ID="btnGo" runat="server" Text="Go" OnClick="btnGo_Click" ValidationGroup="vgGo" />
<asp:GridView...
*.ASPX.CS
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
uc_Counter = 0;
addControl();
gridview_DataBind();
}
else
{
reloadControls();
}
}
protected void btnGo_Click(object sender, EventArgs e)
{
if (Page.IsValid)
{
//THIS BUTTON CLICK IS WHAT'S TRIGGERING THE
//SELECTEDINDEXCHANGED EVENT TO FIRE ON MY *.ASCX
gridview_DataBind();
}
}
private void reloadControls()
{
int count = this.uc_Counter;
for (int i = 0; i < count; i++)
{
Control myUserControl = Page.LoadControl("~/Controls/myUserControl.ascx");
myUserControl.ID = "scID_" + i;
upControls.ContentTemplateContainer.Controls.AddAt(i, myUserControl);
((customUserControl)myUserControl).fillDDLColumns();
}
}
private void addControl()
{
Control myUserControl = Page.LoadControl("~/Controls/myUserControl.ascx");
myUserControl.ID = "scID_" + uc_Counter.ToString();
upControls.ContentTemplateContainer.Controls.AddAt(upControls.ContentTemplateContainer.Controls.IndexOf(btnAddControl), myUserControl);
//((customUserControl)myUserControl).fillDDLColumns();
this.uc_Counter++;
}
protected int uc_Counter
{
get { return (int)ViewState["uc_Counter"]; }
set { ViewState["uc_Counter"] = value; }
}
Even though this is already answered I want to put an answer here since I've recently tangled with this problem and I couldn't find an answer anywhere that helped me but I did find a solution after a lot of digging into the code.
For me, the reason why this was happening was due to someone overwriting PageStatePersister to change how the viewstate hidden field is rendered. Why do that? I found my answer here.
One of the greatest problems when trying to optimize an ASP.NET page to be more search engine friendly is the view state hidden field. Most search engines give more score to the content of the firsts[sic] thousands of bytes of the document so if your first 2 KB are view state junk your pages are penalized. So the goal here is to move the view state data as down as possible.
What the code I encountered did was blank out the __VIEWSTATE hidden fields and create a view_state hidden field towards the bottom of the page. The problem with this is that it totally mucked up the viewstate and I was getting dropdownlists reported as being changed when they weren't, as well as having all dropdownlists going through the same handler on submit. It was a mess. My solution was to turn off this custom persister on this page only so I wouldn't have to compensate for all this weirdness.
protected override PageStatePersister PageStatePersister
{
get
{
if (LoginRedirectUrl == "/the_page_in_question.aspx")
{
return new HiddenFieldPageStatePersister(Page);
}
return new CustomPageStatePersister(this);
}
}
This allowed me to have my proper viewstate for the page I needed it on but kept the SEO code for the rest of the site. Hope this helps someone.
I found my answer in this post .net DropDownList gets cleared after postback
I changed my counter that I was storing in the viewstate to a session variable.
Then I moved my reloadControls() function from the Page_Load of the *.ASPX to the Page_Init.
The key was dynamically adding my user control in the Page_Init so it would be a member of the page before the Viewstate was applied to controls on the page.
I need to create the following functionality, and looking for the best way to store info on multiple postbacks:
Say I have a TextBox, that is related to a question, i.e. "Places I've visited". Once TextBox will be loaded on first load. Under that, there is a button to "Add another place". This will postback, which will then add another TextBox underneath the first. This can be done x number of times.
The question I have is, how do I store/remember the number of controls that have been added to the page by the user, considering that this load needs to be done in the Init event, and ViewState is not loaded at this point?
EDIT*:
I forgot to mention, that when the user saves, there would be some validation, so if validation fails, need to show all the TextBoxes, with their posted data.
If you were going to only allow a finite number of textboxes on your form, you could create that number of textboxes during Page_Init and set their visibility to false so they would not be rendered in the browser. On the button's click event, you could find the first invisible textbox and change the visibility to true. Something like this
protected void Page_Init(object sender, EventArgs e)
{
for (int i = 0; i < 20; i++)
{
this.Form.Controls.Add(new TextBox() { Visible = false });
}
}
protected void addTextboxButton_Click(object sender, EventArgs e)
{
TextBox tb = this.Form.Controls.OfType<TextBox>().FirstOrDefault(box => box.Visible == false);
if (tb != null) tb.Visible = true;
}
Using this approach, the textboxes become visible one by one on each button click, and the postback values stick.
Obviously, you'd want to put some more work into it, such as perhaps definining some literal controls to create line breaks and prompts for the textboxes, as well as displaying a message when the user hit whatever finite limit you set.
Option 1 : You can use Session to store the number of Textboxes...
Option 2 : You can even add controls in the Load event of the page, wherein you will have the ViewState information.
Here are the links that could help you...
TRULY understanding Dynamic Controls
Truly Understanding ViewState
After some thinking, and considering the answers given, came up with this solution.
In the controls placeholder, have a hidden input field which will store the number of controls added to the page. Then, on Init, I can have the following code (testing only):
protected override void OnInit(EventArgs e)
{
int i = 0;
i = Int32.Parse(hdnTestCount.Value);
if(Request.Params[hdnTestCount.UniqueID] != null)
{
i = Int32.Parse(Request.Params[hdnTestCount.UnitueID]);
}
for (int j = 1; j <= i; j++)
{
TextBox txtBox = new TextBox();
txtBox.ID = "Test" + j.ToString();
plhTest.Controls.Add(txtBox);
}
}
protected void btnAdd_OnClick(object sender, EventArgs e)
{
int i = 0;
i = Int32.Parse(hdnTestCount.Value) + 1;
TextBox txtBox = new TextBox();
txtBox.ID = "Test" + i.ToString();
plhTest.Controls.Add(txtBox);
hdnTestCount.Value = i.ToString();
}
Of course the only issue with this, is that the value could be manipulated by the user in the hidden field. The only other option would be to use Session, which I do not want to use as it sticks around, whereby if the page is refreshed this way, the form will reset itself, which is what should happen.
I am adding some checkboxes dynamically during runtime, and I need to know whether they are checked or not when I reload them next time.
I load the checkbox values from a list stored in ViewState.
The question is: when do I save or check for the value of the the Checked?
I tried the event dispose for the check box and the place holder I am adding the checkboxes in, but it wasn't fired. i.e. when I put a break point it didn't stop. So any suggestions?
This is a sample code, but I don't think it is necessary:
void LoadKeywords()
{
bool add = true;
foreach (string s in (ViewState["keywords"] as List<string>))
if (s == ddlKeywords.SelectedItem.Text)
{
add = false;
continue;
}
if (add)
(ViewState["keywords"] as List<string>).Add(ddlKeywords.SelectedItem.Text);
foreach (string s in (ViewState["keywords"] as List<string>))
{
CheckBox kw = new CheckBox();
kw.Disposed += new EventHandler(kw_Disposed);
kw.Text = s;
PlaceHolderKeywords.Controls.Add(kw);
}
}
If you are dynamically adding controls at run time you have to make sure that those controls are populated to the page's Control collection before ViewState is loaded. This is so that the state of each checkbox can be rehydrated from Viewstate. The Page Load event, for example, is too late.
Typically you would dynamically add your CheckBox controls during the Init Event (before view state is loaded) and then Read the values in your Checkbox controls during the Load event (after view state is loaded).
eg:
protected override void OnInit(EventArgs e)
{
//load the controls before ViewState is loaded
base.OnInit(e);
for (int i = 0; i < 3; i++)
{
CheckBox cb = new CheckBox();
cb = new CheckBox();
cb.ID = "KeyWord" + i.ToString();
cb.Text = "Key Word"
MyPlaceHolder.Controls.Add(new CheckBox());
}
}
//this could also be a button click event perhaps?
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (Page.IsPostBack)
{
//read the checkbox values
foreach(CheckBox control in MyPlaceHolder.Controls)
{
bool isChecked = control.Checked;
string keyword = control.Text;
//do something with these two values
}
}
}
Hope that helps
****EDIT****
Forgot to mention that this is obviously just demo code - you would need to flesh it out.
For more information on dynaic control rendering in ASP.Net check out this article on 4Guys.
For more information on the page life-cycle in ASP.Net check out MSDN.
How to:
try adding a javascript code, that handles checked(),
u can get the checkboxes by using document.findElementById(ID) , then store the checkboxe's value into a hiddenfield that has a runat="server" property.
When to:
either on pageload , check if page is postback(), and check the hiddenfield(s) value(S). or add a submit button (and place its event in the code behind, runat="server" property).
hope this helps u.