On Postback, the DataTable data source for a Repeater is empty? - c#

Background
I have a User Control (an .ascx file) which is being dynamically inserting into an asp:PlaceHolder control on a page. That User Control contains an asp:Repeater, which I'm binding to a DataTable.
Theoretically, on the User Control's first load the DataTable is initialized and 3 empty rows are added. A button on the User Control adds additional empty rows to the Repeater, one at a time.
Problem
The issue is that after any PostBack event on the page (namely the button in this example being clicked), the DataTable for the Repeater is empty.
User Control (.ascx)
(simplified)
<asp:TextBox ID="controlOutsideRepeater" runat="server" />
<asp:Repeater ID="myRepeater" runat="server">
<ItemTemplate>
<p><asp:Textbox ID="firstControlInRepeater" runat="server" text='<%# DataBinder.Eval(Container.DataItem, "A") %>' /></p>
<p><asp:Textbox ID="secondControlInRepeater" runat="server" text='<%# DataBinder.Eval(Container.DataItem, "B") %>' /></p>
</ItemTemplate>
</asp:Repeater>
<asp:LinkButton ID="addItemButton" runat="server" Text="Add Item" onclick="addNewItem" />
Code Behind (.ascx.cs)
(also simplified)
public DataTable items {
get {
object i = ViewState["items"];
if (i == null) {
DataTable t = new DataTable();
t.Columns.Add("A");
t.Columns.Add("B");
// add 3 blank items/rows:
t.Rows.Add(t.NewRow());
t.Rows.Add(t.NewRow());
t.Rows.Add(t.NewRow());
ViewState["items"] = t;
return t;
} else {
return (DataTable)i;
}
set { ViewState["items"] = value; }
}
protected void Page_Init(object sender, EventArgs e) {
myRepeater.DataSource = this.items;
myRepeater.DataBind();
}
public void addNewItem(object sender, EventArgs e) {
DataRow r = this.items.NewRow();
this.items.Rows.Add(r);
myRepeater.DataBind();
}
Behavior
The first time the UserControl is loaded, the Repeater contains 3 empty items: good! However, after entering some text in the textboxes both inside and outside the repeater and clicking the "Add Item" LinkButton, the page does a refresh/postback and shows 4 empty items, however the textbox -outside- the Repeater retains it's text. Clicking the "Add Item" LinkButton again also performs a postback and still shows 4 empty items, yet the TextBox outside the Repeater again retains it's text.
My Crazy Guess
I've tried wrapping the Repeater databinding in a (!Page.IsPostBack), but this prevented the Repeater from -ever- being bound, as the UserControl is only programmatically added to the page after a PostBack (a button on the Page adds the UserControl on a click, and then the Page checks each PostBack to see if there should be a user control present and re-adds it to the Page if needed). So I'm guessing there's a problem with the Page re-creating the User Control on every PostBack, but can't explain why the TextBox outside the Repeater would retain it's value, and why the ViewState doesn't seem to remember my item (on each postback ViewState["items"] is null and gets re-built within the getter).
HELP!

The problem is you are data binding every single request when really you only want to data bind on the first request. Since you don't data bind on the first page load, you will have to check if you are data bound in a way other than !Page.IsPostBack. You could add a property to your user control to handle this and then check against that every page load / page init.
Update: With more details from comments
I see your AddItem() now. I've had problems using viewstate this way though I'm not entirely sure why. I've had to do it more like the following:
public void addNewItem(object sender, EventArgs e) {
DataTable theItems = this.items;
DataRow r = theItems.NewRow()
theItems.Rows.Add(r);
this.items = theItems
myRepeater.DataBind(); //I'm not sure if this belongs here because of the reasons said before
}

Related

Dynamically created Items in DropDownList in GridView not working

I have a .ascx user control on an .aspx page. When the user clicks a button, information is gathered and stored in the database. Then a Gridview is databinded and in the Gridview is a dropdownlist. This dropdownlist's Items are created dynamically based on the users previous input, now in the database. This is all good and the Gridview is displayed with the dropdownlist with the dynamically created Items.
The problem is on the postback for this dropdownlist. I obviously have to recreate the Gridview with the dynamically created Items in the dropdownlist. This does not work. The postback happens and calls the Page_Init and then the Page_InitComplete. This has the databind on the Gridview which calls the SectionGV_OnRowDataBound method. The dropdownlists are recreated. But the SectionDD_OnSelectedIndexChanged method is never hit and then the dropdownlist just reverts back to its original value. I can not change the dropdownlist's selected value.
.ASCX
<asp:GridView ID="MyGV" runat="server" AutoGenerateColumns="False" DataSourceID="MyDS" Width="100%" OnRowDataBound="MyGV_OnRowDataBound">
<Columns>
<asp:TemplateField >
<ItemTemplate>
<asp:DropDownList runat="server" ID="SectionDD" AppendDataBoundItems="True" AutoPostBack="True" OnSelectedIndexChanged="SectionDD_OnSelectedIndexChanged" >
</asp:DropDownList>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
Code Behind of .ASCX
protected void Page_Init(object sender,EventArgs e)
{
this.Page.InitComplete += Page_InitComplete;
}
private void Page_InitComplete(object sender, EventArgs e)
{
MyGV.DataBind();
}
protected void SectionDD_OnSelectedIndexChanged(object sender, EventArgs e)
{
}
protected void MyGV_OnRowDataBound(object sender, GridViewRowEventArgs e)
{
using (EntitiesModel dbContext = new EntitiesModel())
{
// get array[][] array from database
if (e.Row.RowType == DataControlRowType.DataRow)
{
DropDownList sectionDd = (DropDownList)e.Row.FindControl("SectionDD");
sectionDd.Items.Clear();
if (array.Length == 3)
{
if(array[0][2].ToDecimal() > 0) sectionDd.Items.Add(new ListItem(array[0][0], array[0][1]));
if(array[1][2].ToDecimal() > 0) sectionDd.Items.Add(new ListItem(array[1][0], array[1][1]));
if(array[2][2].ToDecimal() > 0) sectionDd.Items.Add(new ListItem(array[2][0], array[2][1]));
}
else if (array.Length == 2)
{
if (array[0][2].ToDecimal() > 0) sectionDd.Items.Add(new ListItem(array[0][0], array[0][1]));
if (array[1][2].ToDecimal() > 0) sectionDd.Items.Add(new ListItem(array[1][0], array[1][1]));
}
else if (array.Length == 1)
{
if (array[0][2].ToDecimal() > 0) sectionDd.Items.Add(new ListItem(array[0][0], array[0][1]));
}
}
sectionDd.DataBind();
}
}
}
}
private void SaveToDB()
{
//save information to database
NOTE
This is one of many ways I have tried to solve this issue. The reason I have saved the information in the database is just a temporary fix. I just want to solve the issue outlined above and then I will add a ViewState solution.
First up, no, you do not have to re-create or load the grid again, or the dropdown list in the post back.
The user can type into the grid - change values, select drop downs. At that point you can loop all the grid rows and get/grab the dropdown values selected.
You CAN of course fire/trigger a event for the dropdown, but it not at all clear if you really need that event here.
If your grid is getting messed up on post-backs?, then it means you not limiting the load up in the FIRST page load - after that, it should not matter.
Say we have this grid markup:
<asp:GridView ID="MyGrid" runat="server" CssClass="table table-hover"
DataKeyNames="ID" AutoGenerateColumns="false" OnRowDataBound="MyGrid_RowDataBound" >
<Columns>
<asp:BoundField DataField="FirstName" HeaderText="FirstName" />
<asp:BoundField DataField="LastName" HeaderText="Last Name" />
<asp:BoundField DataField="HotelName" HeaderText="Hotel Name" />
<asp:TemplateField HeaderText="City">
<ItemTemplate>
<asp:DropDownList ID="DropDownList1" runat="server"
DataTextField="City"
DataValueField="City"
>
</asp:DropDownList>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="Province" HeaderText="Province" />
</Columns>
</asp:GridView>
And of course non templated fields (such as the first few boundField - they appear in the .Cells collection. but for templated columns, you use findcontrol.
However, in EVERY and NEAR ALL web pages, as a general rule, you ONLY load + mess + create the grid ONE time, and you ONLY do this on the first page load - END OF STORY! You don't follow this rule, then you are in a world of hurt big time.
Ok, so lets load up the grid. Since we have a dropdown, then we have to fill that out on item data bound.
So, our code will look like this:
public DataTable rstCity = new DataTable();
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack == false)
{
LoadGrid();
}
}
public void LoadGrid()
{
using (SqlCommand cmdSQL = new SqlCommand("SELECT City from City Order by City",
new SqlConnection(Properties.Settings.Default.TEST4)))
{
// locd city for drop down list
cmdSQL.Connection.Open();
rstCity.Load(cmdSQL.ExecuteReader());
// now load grid
cmdSQL.CommandText = "SELECT * from tblHotels ORDER BY HotelName";
DataTable rst = new DataTable();
rst.Load(cmdSQL.ExecuteReader());
MyGrid.DataSource = rst;
MyGrid.DataBind();
}
}
Note how I did scope the city table to the class level (no reason to load it over and over for each row - and after the first page load, it will go out of scope - we don't care - it will live during the first page load and the data binding.
Ok, now, lets do the item data bind for the grid.
We have this:
protected void MyGrid_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
DropDownList MyDrop = (DropDownList)e.Row.FindControl("DropDownList1");
MyDrop.DataSource = rstCity;
MyDrop.DataBind();
// get City from current data row source - set the drop to data row City
MyDrop.SelectedValue = ((DataRowView)e.Row.DataItem)["City"].ToString();
}
}
Ok, that's it. (note how the "dataitem" exists ONLY during data binding - a handy tip, since then you have use of the FULL data row - including PK values, and columns that you don't even include in the grid markup. But, once data binding is over, then DataItem can NOT be used - it only EVER exists during the bind process. But this information is VERY valuable, since you have full use of the Actual data SOURCE you used to bind. In above, I needed the City from that Row to set the drop list.
Ok, the output now looks like this:
Ok, at this point since we ONLY bind on first page load. The view state of that grid is 100% automatic handled by asp.net for you at this point in time.
You can drop other buttons on this form - post backs should NOT matter. The BIG lesson here is that gridview does persist (at least it will if we don't re-load it each time on post-back - you should NOT have to re-load).
Ok, next issue:
In most cases, I don't see the need for the dropdown list event in the grid?
So, you can as a general rule select and change any row combo. Once done, then you can get values of each row by looping the data grid rows - including that of dropdown selected.
However, Lets wire up the dropdown list event anyway.
tip of the day:
Since we can't select the dropdown and use the property sheet?
Then in markup do this:
You can in the markup type in OnSelectedIndexChanged=
WHEN you hit the "=" sign, note VERY close how intel-sense pops up a event create option:
So, click on CreateNewEvent - it "seems" like nothing occurs, but in fact if you flip over to code behind, you have a nice event stub created.
So lets put our code in that event - grab the row - show the value just slected.
protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
// user changed the combo box
DropDownList MyDrop = (DropDownList)sender;
GridViewRow gRow = (GridViewRow)MyDrop.Parent.Parent;
Response.Write("<h2>Row index is = " + gRow.RowIndex.ToString() + "</h2>");
Response.Write("<h2>Hotel Name is = " + gRow.Cells[2].Text + "</h2>");
Response.Write("<h2>City from Drop selected = " + MyDrop.SelectedValue + "</h2>");
}
Of course we set auto post back = true in the drop markup also - right?
And note how we pick up the "sender", and then use .Parent.Parent to get the grid row that we are operating on. The first parent is some cell or some such.
I actually built recursive function to get that value, but for here we just used .parent.Parent (often with a extra markup, then you need to go op one more .parent).
Anyway, now say I select the last drop down in above, and change it.
I get this output:
In summary:
Only EVER load the grid - first page load - (you check IsPostBack).
You can have all kinds of additional post backs - the grid should survive and just be fine - no need to re-load at all. (GridView has built in viewstate).
Now, after I have fun, change the drop down on many rows?
I can loop the gridview - and any changes to that grid will persist.
In fact, if you make the columns text boxes (in templated fields), then you can tab around and edit almost like Excel, and again the values will persist for you, and they survive post backs. (and then you can send the whole grid of changes back to the database in one update command - I can show how to do this, but this post already has lots of good stuff anyway.
Now, having said the above, having done the above?
Well now that we have this working, then one can go back to building a custom user control - but if done right, it also should behave correctly, and should also survive post-backs.

Cannot get new value of control in popup modal

Do web controls ever appear like you are changing their values but actually retain the previous value?
I created a pop-up modal for users to edit an item. When the user clicks edit on an item on the main page, the following sequence happens:
The item's ID is passed to the Page_Load event of the modal page, and is used to populate the page control's with the item's data.
The user changes a value in a control. Ex: Changes text in a TextBox contol.
The user clicks save, triggering the Click event which creates a DataTransferObject with the values in the textboxes, which will be stored.
However, on step 3, the control's new value (TextBox.Text) still holds the value that it orginially had, not the value the user put in.
Add.aspx:
<%# MasterType VirtualPath="../MasterPages/Popup.Master" %>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
<asp:TextBox ID="TextBoxDescription" runat="server"></asp:TextBox>
<telerik:RadButton ID="btnSave" runat="server" Text="Save" OnClick="btnSave_Click"/>
</asp:Content>
Add.aspx.cs
//Cannot access the new values here
protected void btnSave_Click(object sender, EventArgs e)
{
//This will print the new text on Create, but the old text on Edit
System.Diagnostics.Debug.WriteLine(TextBoxDescription.Text);
}
//works properly
protected void Page_Load(object sender, EventArgs e)
{
objIDParam = Convert.ToInt64(Request.QueryString["ObjectID"]);
editMode = (objIDParam != 0) ? true : false;
if(editMode)
PopulateFields(objID);
}
//works properly
private void PopulateFields(long objID)
{
MyObject obj = GetObjectByID(objID);
TextBoxDescription.Text = obj.Description;
}
It is worth noting that this popup page is used for both creating items AND editing items. Create works fine (i.e. The item isn't saved with all blanks, but rather the user input). Editing an item will properly pull all that data back in, and let the user edit the fields, however I can't access the changed values in my code.
You need to check for IsPostBack in the Page_Load method.
The Page_Load gets called before the btnSave_Click method, so the TextBoxDescription.Text is getting reset to obj.Description before the btn_Save method runs.
Try returning out of Page_Load if you're posting back:
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
return;
objIDParam = Convert.ToInt64(Request.QueryString["ObjectID"]);
editMode = (objIDParam != 0) ? true : false;
if(editMode)
PopulateFields(objID);
}
Have a look at ASP.NET Page Life Cycle Overview for more info.

Passing value from Page to UserControl

Good day. I have a trouble with pass data between webform and UserControl.
I have the webform webform1.aspx. And i have a component GridView1. I also have UserControl - grdControl.ascx.
In webform1.aspx i mention UserControl:
<uc1:grdcontrol runat="server" id="grdControl" />
Also webform1.aspx.cs contains an event
protected void GridView1_SelectedIndexChanged(object sender, EventArgs e)
{
grdControl.SelectedValue = GridView1.SelectedValue.ToString();
}
In turn UserControl grdControl.ascx.cs contains the following code:
private string _selectedValue;
public string SelectedValue
{
get { return _selectedValue; }
set { _selectedValue = value; }
}
In grdControl.ascx i use label and i try to get value SelectedValue and use it as text for Label1.
<asp:Label ID="Label2" runat="server" Text='<%# SelectedValue%>' ></asp:Label>
But is don't work correctly. In a web page i see nothing.
Setting the value of the label in the PreRender event of the code behind should fix it.
I'm thinking that GridView1_SelectedIndexChanged will get called after the Page_Load event of the inner grid control that's why the value is blank because you are trying to set it in the page itself and that happens in the Pre_init function of the inner control. I don't think SelectedValue would be set at that point.

Manually bind a row in a repeater, to avoid losing viewstate data

I'm working on a page that uses a repeater to display a list of custom controls, each containing two dropdown lists.
On a click on the Add control button, the page adds a new row on the repeater, and a click on one of the Delete control buttons embedded in each control removes the relevant control from the repeater.
The delete part seems to work, (setting NamingController.Visible to false), but the add part fails, as once I add the new control, a call to a new repeater.DataBind() loses all viewstate data, preventing the dropdownlists from retrieving the values they had before postback.
Is there a way to manually bind the added control to the repeater without calling a full databind ? Or is there any other way to add a control without losing data ?
Here's some code (I only left what seems relevant, please let me know if you think I forgot to specify something) :
Page.aspx:
<asp:Button ID="addControl" runat="server" Text="Add control" />
<asp:Repeater ID="repeater" runat="server" OnItemDataBound="repeater_ItemDataBound">
<ItemTemplate>
<uc:CustomControlWithDropDownLists ID="custom" runat="server" />
</ItemTemplate>
</asp:Repeater>
Page.aspx.cs:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
repeater.DataSource = GetDataSource();
repeater.DataBind();
}
protected void Page_Init(object sender, EventArgs e)
{
addControl.Click += (sndr, args) =>
{
// Create the object we want to bind to the repeater
ObjectToBind objectToBind = new ObjectToBind();
// Here is what causes data loss
((IList<ObjectToBind>)repeater.DataSource).Add(objectToBind);
repeater.DataBind();
};
}
protected void repeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
{
// Do some stuff
}
}
CustomControlWithDropDownLists.ascx:
<%-- Some dropdown lists --%>
<asp:Button ID="deleteControl" runat="server" Text="Delete control" />
CustomControlWithDropDownLists.ascx.cs:
protected void Page_Init(object sender, EventArgs e)
{
deleteControl.Click += (sndr, args) =>
{
// ... Delete the control ...
((Button)sndr).NamingContainer.Visible = false;
};
}
You are loosing the data because of View state.
View State losses the data when we are redirecting to the another page.
View state only store the data for the specific page.
The moment you go to another page data is lost by view state.
& there are many chances of losing the data on the same page because of many reasons.
So, the best way to store the data is to use session variable.
Session stores the data even when you are redirecting to the another page.
It is the best way to store the data.
By default it stores the data for 20 minutes.
I hope this will solve your issue.
I had done something similar in the past and found this link from c-sharp corner helpful.
http://www.c-sharpcorner.com/Blogs/10913/add-dynamic-row-using-repeater.aspx
The key element for me if memory servers is using the ViewState object and binding accordingly.
myObject = ViewState["MyData"]; etc.
( I am sorry I don't have access to my code at the moment )

Dynamically added items don't raise events.(ASP.NET ListBox)

My code is:
private void Add_Items()
{
for (int x = 1; x < 53; x++)
{
ListBox1.Items.Add("Item" + x);
ListBox1.DataValueField = "Value" + x;
}
}
None of these items raises SelectedIndexChanged event when clicked.
Please assist.
Make sure autopostback is enabled, like in this examle:
<asp:ListBox ID="listBoxLocation" runat="server" AutoPostBack="True"
OnSelectedIndexChanged="listBoxLocation_SelectedIndexChanged" EnableViewState="True">
<asp:ListItem>1</asp:ListItem>
<asp:ListItem>2</asp:ListItem>
<asp:ListItem>3</asp:ListItem>
</asp:ListBox>
Or to dynamically populate:
Protected void Button1_Click (object sender, System.EventArgs e)
{
ListBox1.Items.Add(new ListItem("Carbon", "C"));
ListBox1.Items.Add(new ListItem("Oxygen", "O"));
}
From: http://msdn.microsoft.com/en-us/library/14atsyf5%28v=vs.85%29.aspx
First.
The ListBox1.DataValueField should not be set to every item. This property sets the field on the data object (each row) to capture the value from. Here is the MSDN link for this property http://msdn.microsoft.com/en-us/library/system.web.ui.webcontrols.listcontrol.datavaluefield(v=vs.110).aspx.
Next, I am assuming you have all the front end code wired up something like.
<asp:ListBox ID="ListBox1" OnSelectedIndexChanged="ListBox1_SelectedIndexChanged" runat="server"></asp:ListBox>
This has the selected changed event wired up. However for this control to actually Post the data back you need to provide one more attribute. Add
AutoPostBack="true"
To your control as.
<asp:ListBox ID="ListBox1" OnSelectedIndexChanged="ListBox1_SelectedIndexChanged" AutoPostBack="true" runat="server"></asp:ListBox>
This starts the magic. MSDN for AutoPostBack: http://msdn.microsoft.com/en-us/library/system.web.ui.webcontrols.listcontrol.autopostback(v=vs.110).aspx
Your code is very vague, and im going to make the assumption that your ListBox has not been linked up to a SelectedIndexChanged event in the correct way.
If you are dynamically creating the ListBox link it up as below:
public void initialize()
{
ListBox lb = new ListBox();
lb.SelectedIndexChanged += lb_SelectedIndexChanged;
}
private void lb_SelectedIndexChanged(object sender, EventArgs e)
{
//Do Selected Index Changed Code Here
}
If you have a view/form with the control on, simply make sure that the ListBox's event has been set or naturally it will not trigger.
*EDIT 1
As noted in other answers your control is required to have PostBack set. You should also be checking the PostBack state of your page to ensure you are not redrawing your controls continuously, as this will keep resetting your Dynamically added controls.

Categories