I ran into a problem recently when using a repeater that I was adding dynamic controls into and although I've got a workaround that does functionally exactly what I want it to do, I'd like to know if there is a better way to do it for my understanding. I've been working with ASP.NET for about 6 months now, and everytime I think I've got the page lifecycle/viewstate completely sussed something crops up that I can't answer.
I was creating a form where users could register their interest for an event, and register for multiple people.
The aspx went something like:
<asp:Repeater ...>
<bunch of formatting and always there controls like firstname/lastname/address>
<asp:PlaceHolder ...>
<dynamic controls for workshop selection go here>
</asp:PlaceHolder>
</asp:Repeater>
An event can have workshops that the user can register for, and the availability of the workshops is dependant on the date that they choose to go to the event on. The availability of the workshops is dependent on the date, so they can't choose the workshops until they've selected a date.
Anyway the dynamic controls that I'm adding are basically a bunch of literals and a bunch of radio button groups.
I started off by adding the controls in the ItemDataBound event handler, but when saving my repeater items back to my delegate list the ViewState of the radio buttons was never updated. All the fields that were referenced in the ItemTemplate were handled fine, but not the radio buttons I was adding dynamically.
I tried to use the ItemCreated event instead, adding my buttons there, but that didn't seem to make any difference. In the end I settled on a workaround which was based off of this. Basically I'm just outputing the HTML input fields in a literal and reading them back from the request.
It all works perfectly now, and I'm happy with the functionality, it just seems really dirty to have to output the radio buttons as HTML inputs directly rather than use a server side control. Does anyone have a clue about why the ViewState wasn't being restored properly?
Just to be clear, the controls were recreated in the same order everytime, with the ID properly set. I'm using dynamic controls all over the place and they're working fine, I just can't get them to work in this case when I'm adding them inside a repeater.
//more clarification
I can't create these controls in Page.Init as the user selects the date which causes a postback and I have to wait for the viewstate of that control to load before I create the dynamic controls.
You're creating (or re-creating) the controls when the repeater is bound to its data source. If this happens after the ViewState has been loaded by the page, the ViewState won't be available to the dynamically-created controls.
Check you're binding your repeater early enough - Page.Init is OK; Page.Load is too late.
It is worth noting that Radiobuttons controls do not work 'out of the box' in repeater controls. Repeater controls inherit the INamingContainer interface which ensures all rendered html controls have unique name attributes. As radio buttons use the name attribute to group themselves this means you will not be able to create groups of radio buttons i.e. setting the GroupName property will not have the desired effect as the Repeater will override this and create unique names for each RadioButton.
Related
I'm trying to build a very specific search page for a project and I'm having lot of trouble dealing with multiple postbacks invoked by dynamically-generated controls on a single page.
The page has to work like this:
There is a single checkbox, "Detailed search", that causes a postback on checking/unchecking.
When detailed search is not active, a simple grid with contents and buttons is displayed. Nothing special.
When detailed search is active, N checkboxes must be generated from some dynamic data, that represent the sections where you want the search to happen. Below the checkboxes, an AJAX-enabled tab control will appear, initially with no tab pages.
When checking one of the section checkboxes, a postback will occur. After the postback, data will be searched in the section selected by the user, then a new tab page containing a grid view of results and the name of the section will be added to the tab control. If the checkbox is unchecked, the tab page will disappear from the control, again, after a postback.
Now, the issue is that pretty much everything has to be generated dynamically, and that pretty much everything is connected to something else.
First issue: dealing with the "Detailed search" checkbox. Sounds easy, doesn't it? My initial idea was to set Page.Viewstate["DetailedSearchEnabled"] to true or false during the check/uncheck event handler, then create controls dynamically checking the value of DetailedSearchEnabled during Page_Load.
Nope. The postback event-handling happens between Page_Load and Page_LoadComplete. It would take an additional refresh for things to work as intended.
<< Then I'll just generate the controls on Page_LoadComplete! >>
Nope. Those controls need event handling as well, and if they're generated after Page_Load they will not be wired up correctly.
A possible solution would be generating everything in advance, on Page_Load, and only hiding/showing controls on Page_LoadComplete. But that is inefficient, and one important point of this search page is that only the minimum amount of controls should be generated.
The difficulty of this task seems to come from the way event wiring and the page life cycle work.
Surely there must be a better way of approaching this problem.
First issue: dealing with the "Detailed search" check box.
The correct approach (if you want to use page post-backs) is as follows:
In the CheckChanged event handler, save the value of the Checked property to ViewState["DetailedSearchEnabled"]. If the value is true, add the dynamic check boxes to the page. If the value is false, find and remove them.
Override LoadViewState. After calling base.LoadViewState, re-create the dynamic check boxes and wire up their events if ViewState["DetailedSearchEnabled"] is true. Note that neither Page_Load nor Page_LoadComplete is the appropriate place to do this.
Yes, you should create the dynamic check boxes at two points in the page life cycle. I recommend a helper method.
In general, your event handlers should add or remove just the dynamic controls (if any) affected by those particular events, but LoadViewState should re-create all dynamic controls that existed from the previous page request. You must store enough information in view state for LoadViewState to do so.
My answer to this other question demonstrates how to add and remove dynamic controls. You may want to use it as a reference.
Sounds to me like you should be using a CheckBoxList control to handle your dynamic checkboxes. You can add an remove items to the CheckBoxList during your post back and not have to worry about dynamically adding/removing actual controls/events to the form.
Here is a link to the msdn:
https://msdn.microsoft.com/en-us/library/14atsyf5(v=vs.85).aspx
Here is some sample code:
Protected void Button1_Click (object sender, System.EventArgs e)
{
CheckBoxList.Items.Add(new ListItem("TextValue1", "Value1"));
CheckBoxList.Items.Add(new ListItem("TextValue2", "Value2"));
}
If all else fails, you could still fall back on the quick-and-dirty old-fashioned ASP way.
Use Response.Write or <%...%> to generate your dynamic controls as plain old HTML (simple form fields, e.g. <input type="checkbox" name="foo" value="1" />).
Make sure you have a form field for every piece of information you may need after the postback(s). If necessary, use hidden form fields to 're-post' values across subsequent postbacks.
After postback, retrieve the values of the controls with the Request object.
Use those values to adjust the generation of controls as you see fit.
You should be able to do all of this in Page_Load. The advantage is total freedom. The disadvantage is total freedom to make a big mess of your aspx. So you may want to migrate all this dirty code out of your aspx, and into a custom-made control, which you can then add to your aspx.
When generating your own HTML, be careful not to introduce XSS vulnerabilities. Use HtmlEncode where necessary.
As you suggested yourself, there is a better way to tackle it.
If I was in the same situation, I would create web methods for interacting with the page, and use client side to do the UI. I'm currently working mostly with angular JS, although it does come with a learning curve. You could use ng-hide/ng-show to bind to the checkbox event to display the detailed search. When the n number of checkboxes needs to be displayed, you can then just fill them in with ng-repeat, for each of the items you need to display, after a check/uncheck you can dynamically populate new controls etc. through web method calls if extra data is needed.
Pure ASP postbacks are quite clunky from my experience, and not really suited for building a maintainable dynamic UI.
Instead of making so many postbacks, it would be better to use jquery and ajax calls to load the controls as needed and then attach events to it or you can even use UpdatePnael for that. Help Links:
https://www.simple-talk.com/dotnet/asp.net/ajax-basics-with-jquery-in-asp.net/
http://encosia.com/using-jquery-to-directly-call-aspnet-ajax-page-methods/
I've got a Repeater control, bound to a PagedDataSource, which datasource is a list of custom controls I've made. These custom controls contains a couple of text boxes.
I have a save button, and when it is clicked I want to save the data in all the custom controls to a database, no matter which page they are on - but currently I only got access to the custom controls displayed on the current page.
What I've tried to do is to, in the btnSave_Click event, create a new temporary datasource equal to the current one, except its not a PagedDataSource. That way my repeater contains all custom controls - BUT - the changes made in the textbox fields are no longer available. I then tried to add JavaScript onchange events on the textboxes in the custom control, so that a postback would be fired whenever text was changed, and the property in the user control codebehind would be updated. This didnt work either.
Any ideas?
save the changed values on each page index changing event (or prev /next buttons) into your persistance object (List)
http://www.dotnetfunda.com/articles/show/1611/how-to-select-multiple-records-from-multiple-pages-of-the-gridview-and
The reason your non-PagedDataSource is empty is because the changes in your text box exist in the client and not on the server - you'll need to synchronise the values from your controls with the empty slots in your repeater.
The Repeater does not have built-in Pagination (like the GridView or other complex controls) so it does not offer events such as the PageIndexChanging event. I assume therefore, that you have your own Page navigation implementation. You should therefore call the function you have presented within that implemented function.
Try Using a generic List and Skip and Take methods of that
I have a repeater control that pulls back some data from a database and creates a radiobuttonlist for values related to each row. I don't know how many rows will be coming back, so I don't know how many radiobuttonlists there will be. This repeater is inside of a form, and once I click "Submit" on the form I need to get the values from the radiobuttons generated by the repeater.
Any thoughts on how to do this?
There are a few options.
One is to use a nested Repeater to display the RadioButtons.
The other is to create the controls during the original repeaters. Repeater.ItemDataBound event.
In either case, you need to be very aware of the Page Lifecycle.
You'll want to avoid databinding in the Page_Load event, unless the databinding is being done with an if(!Page.IsPostback) block. If you fail to check for postback, you'll run into issues where the Page_Load calls a DataBind() call that then wipes the values of the repeater before any event handlers can use it.
Alternatively, you can do your binding in Page_Init, which occurs BEFORE the viewstate can be applied, so you won't lose your values.
You can also use USer Controls for the RadioButtons. There's a previous post that covers how to use nexsted user controls within a Repeater here.
Related question: Get All Web Controls of a Specific Type on a Page
In the question above I asked how I can get all controls, works like a charm but something just doesn't quite fit so I thought it might be me. I have the following code but it's not manipulating the controls on the page but in my theory it should work.
List<DropDownList> allControls = new List<DropDownList>();
ControlEnhancer.GetControlList<DropDownList>(Page.Controls, allControls);
foreach (DropDownList childControl in allControls)
{
foreach (ListItem li in childControl.Items)
{
li.Attributes.Add("title", li.Text);
}
childControl.Attributes.Add("onmouseover", "this.title=this.options[this.selectedIndex].title");
}
Thats the code, GetControlList() code you can get from the related question which shows how it gets all controls, its just my manipulation. I am trying to get all dropdownlist listitems and add a title to them so I can have a tooltip.
It's a quick fix for IE8 and below which cuts of long text in drop down boxes.
Page_Load happens often too soon; Page_PreRender is the last moment before the page's HTML is actually rendered for the browser and in many cases is the best place to set attributes on user controls.
This because during the web form (page) life cycle there are other events in the page (and in the user controls contained in the page...) which sometimes remove/replace/overwrite (really) those attributes so the only way you can get those attributes to the browser is to append them after all other life cycle events have been fired and handled, in the Page_PreRender.
Actually, even PreRender might be too early in some cases (e.g. you could have DropDownList controls added to the control tree during databinding of controls that use DataSourceID).
There are two further events that might be more appropriate:
PreRenderComplete. At this point, all controls are created and the page is ready to render.
SaveStateComplete. Occurs after view state and control state have been saved. Any changes you make here won't be persisted to view state.
In your example (adding client-side attributes), I'd use the SaveStateComplete event to avoid unnecessary view state bloat.
I want to make use of "complex" usercontrols with more than one control element within. It's the same control I will reuse in the list, and I have a PlaceHolder control for it already.
I can add the control with LoadControl(path to .ascx) - no problem.
I can through my custom properties get/set access the embedded Labels, too, so I can initialize each control perfectly.
But when adding LinkButtons, I get into trouble/problems.
When I click the button, I do get a "submit" of the page rendering the controls; but the control's own button event does not seem to fire (or at least PageLoad on the parent page seems to fire first?) - I can't figure out where my event goes or where to look for a name/ID or parameter for this button.
How come or what am I doing wrong here?
I've made a "fake button" now by using a label more with a "hardcoded A HREF" with an ID in the URL, but I would like to learn what event I need to catch and where or how to init the button, because I want to be able to use "default ASP.NET" controls for these usercontrols (hopefully without too much patchwork-coding)...
The only reason that events get "lost" is because your controls are not being recreated in such a manner that ASP.Net can associate the event with the control after the postback. It does so through the use of the ID property.
In other words, you're doing one of three things wrong:
1) You're assigning the ID's of your linkbuttons differently during the creating phase in Init after the postback
2) You're creating your linkbuttons dynamically using code, but you're doing it after the Init phase of the page lifecycle, so that your controls do not participate in ViewState.
3) You're re-binding the datasource of the parent control containing the linkbuttons on every postback. Use if (!IsPostBack) to prevent rebinding it every time.
Without seeing your code I can't give anything more specific than that unfortunately.