alt text http://img3.imageshack.us/img3/1488/advancedsearch.png
I'm building an "Advanced Search" interface in an ASP.NET application. I don't need SO to write this thing for me, but I'm stuck on a specific problem regarding dynamic controls and ViewState. I would like some direction for how to approach this. Here's my situation:
Ingredients:
A serviceable set of API objects representing entities, fields, and searches, which handles constructing a search, generating SQL, and returning the results. So that's all taken care of.
ASP.NET 3.5
Desired Interface Functionality:
(1) On initial page load, the interface gets a preconfigured Search object with a set of SearchCriterion objects. It binds them into a set of controls (see image above.)
Some search items are simpler, like:
Field (DropDownList) | Operator (DropDownList) | Value (TextBox)
Search Criterion controls for some field types have important information stored in viewstate, like:
Field (DropDownList) | Operator (DropDownList) | Value (DropDownList) where the "Value" dropdownlist is populated by a database query.
Some fields are lookups to other Entities, which causes a chain of field selectors, like:
Field (DropDownList) Field (DropDownList) | Operator (DropDownList) | Value
(2) The user modifies the search by:
Adding and Removing search criteria by clicking respective buttons
Configuring existing criteria by changing the Field, Operator, or Value. Changes to Field or Operator will require the control to reconfigure itself by changing the available operators, changing the "Value" input control to a different type, or adding/removing DropDownLists from the "Fields" section if Lookup-type fields are selected/unselected.
(3) Finally, the user hits "Search" to see their results.
The Problem:
As you probably already know if you're answering this question, controls added dynamically to the page disappear on postback. I've created a UserControl that manipulates the control collection and neatly accomplishes step (1) above as you can see in the attached image. (I'm not concerned about style at this point, obviously.)
However on Postback, the controls are all gone, and my Search API object is gone. If I could get the dynamically generated control collection to just play nice and stick in ViewState, I could examine the controls on postback, rebuild the Search object, then handle control events neatly.
Possible Solutions
I could make the Search object serializable and store it in viewstate. Then on page load I could grab it and reconstruct the control collection at page load time. However I'm not sure if this would play nicely with controls raising events, and what happens to the viewstate of Drop-down lists that contain data from the database - could I get it back? It's highly undesirable for me to have to re-query the database on every postback.
I could develop a custom server control (see this link) for this kind of thing... but that is a new topic for me and would involve some learning, plus I'm not totally sure if a custom server control would work any more nicely with non-fixed control collections. Anybody know about that?
I was thinking that I might be able to accomplish this using databound controls - for example I could bind my criterion collection to a repeater which has a fixed control collection (maybe hide the non-used "value" controls, use an inner repeater for the "Field" drop-down lists). Then all the information would stay in ViewState... right?
Any new ideas would be greatly appreciated.
thanks for your help.
b.Fandango
I've been coding for about a day and I got this working beautifully using the third option I suggested in my question - old-school databound controls. Actually I only thought of the idea when I was forced to write out the question in detail - doesn't that just happen to you all the time?
I put my SearchCriterionControl into an asp:Repeater and bound it to my object collection. For the Field Chooser I put an asp:DropDownList inside a nested asp:Repeater and bound the Field array to that. Everything works beautifully, keeps state, actually required very little code. So I never had to dynamically add controls to the page, thank goodness.
Thanks for your suggestions, Ender, Matt and andrewWinn.
Since no one else has taken a stab at this for 2 hours, I'll throw my hat in the ring with a solution that does not rely on viewstate at all (or the ASP.NET model of postbacks).
What if you grabbed all the input values with jQuery and instead of doing a post-back did a post against the page (or a new results.aspx page)? Or, you could make the entire thing asyncrhonous and do an Ajax request against a web method, get fed the results, and populate on the client side as needed?
The unfortunate thing here is you have to reconstruct which type of controls were used to figure construct your search query since that data wont be passed with the viewstate. But I imagine you were already going to have to do some kind of translation of your input data into a query form anyway.
Read here for more information about using jQuery to hit an ASP.NET page method. Remember - page methods must be static (it's an easy oversight).
I'm not sure what you're doing server side to construct your query - but I would highly recommend LINQ. I did a similar "advanced search" function previously, and after a few different attempts found that LINQ was a wonderful tool for this problem, regardless of whether I was hitting SQL with LINQtoSQL or just hitting an in-memory collection of objects.
This worked so well because 1) LINQ is deferred execution and 2) A LINQ query returns another queryable object. The implication here is that you can chain your LINQ queries together as you construct them from your input, instead of having to do a single massive clause translation to SQL or whatever backstore you are using (one of my attempts was constructing SQL clauses with strings, but still passing input data via SQLParameters for SQL injection protection - it was messy and complicated when hand crafted LINQ was orders of magnitude easier to understand and implement).
For example:
List<string> data; // or perhaps your a DB Context for LINQtoSQL?
var query = data.Where(item => item.contains("foo"));
if( {user supplies length search option} )
query = query.Where(item => item.Length < 5);
// etc, etc.
// LINQ doesn't do anything until the query is iterated, at which point
// it will construct the SQL statement without you worrying about details or parameter binding
foreach(string value in query)
; // do something with the results
Because of deferred execution and the queryable return type, you can concatenate LINQ queries to this expression all day long and let it worry about the implementation details (such as converting to a SQL query) at execution time.
I can't provide you with the exact steps that you will need to do, but I HIGHLY suggest looking into asp.net page life cycle. I created a user control as a DLL one time. I had to capture postback data at specific steps in the lifecycle and recreate and rebind the data at other steps. Additionally thinkgs like viewstate are only available at certain points also. I know that I had to override On_init, On_prerender and some other methods.
Sorry I couldn't be more help, but I don't have the code with me (its with an old employer). I hope this helps.
If you are adding controls to the controls tree dynamically, you need to add them on postpack as well. Just call the method that builds the control on Page_Load or Page_Init and the controls should stay on the page on postback.
Related
I use GridView & Repeater (and other like DropDownList) controls extensively in my application along with ObjectDataSource components and classes that serve data for ObjectDataSource (TypeName / SelectMethod attributes on ObjectDataSource). I recently noticed that SelectMethod is only called when
IsPostBack == false
Also, when I bind manually, I always bind when !IsPostBack. I never was curious how controls maintain their data between postbacks, until now (I have to create GridView with sorting/pagination etc and I want to do it efficently).
Could you explain / provide some links with descriptions how it's done?
Also I don't get one thing: when working with GridView and I iterate over rows, sometimes I need to access its GridViewRow.DataItem property in order to get backing object (usually to get some kind of ID). And sometimes it's null - sometimes not. I couldn't figure out why. However then it's null, I still can access (for instance) GridViewRow.DataKeys.
Thanks
EDIT: know when answers say that it thanks to ViewState, I have another question: If I data bind 100 business objects which are pretty heavy and I only use a few properties while data binding (let's say I also use OnRowDataBound event to render some additional data), does it mean that whole objects are serialized?
ASP.Net uses ViewState a hidden variable in the all HTML pages to maintain state of a page.
Reasd this for more understanding
http://www.google.co.in/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&cad=rja&sqi=2&ved=0CFMQFjAB&url=http%3A%2F%2Fmsdn.microsoft.com%2Fen-us%2Flibrary%2Fms972976.aspx&ei=UpE0UK3uBsOqrAe94ICwBg&usg=AFQjCNG5ErbrFH0ZYV_WW-jonIl25xEsDQ
They store it in a hidden field on the page called ViewState (or more precisely "__VIEWSTATE"). If you do a View Source on your ASPX page, you'll see it.
You too can store and retrieve state information for your page in the ViewState by accessing the ViewState property of your Page.
I'd start here http://msdn.microsoft.com/en-us/library/ms972976.aspx
So, yes, if you store a large number of large objects, there is a large amount of data being transferred to and fro to the client in the page cycle. Used carefully, Viewstate can be useful, but used carelessly, it can have negative effects.
I have recently started to use Nhibernate and i am quite happy with it until i needed to BIND to ASP.NET controls. I was having major issues binding a gridview to a collection of Products (IList). In the end i was forced to right a small routine to convert my IList to a DataTable. Once it was in datatable it worked flawlessy.
Now has come the time to bind a standard Dropdownbox to 1 field of a collection (IList) of Products but it appears i am having issues again.
So this has brought me to the conclusion that i must be doing something wrong?
I can't believe that it isn't possible to BIND ASP.NET controls to a collection (IList) of a class (in my case products) that is returned from NHibernate.
I would really appreciate any feedback anyone has on the situation... I am at a loss
Thank you
The problem is not that you can't bind, because you can. Generally issues like this come about when you're binding at the wrong time.
NHibernate supports laziness. So if your query is lazy, and properties on the returned objects are lazy, then the values won't be pulled from the database until the items and properties are referenced. If you bind these to controls in the UI, then the values won't be extracted until the page gets rendered.
At this point there is a good chance that you have already closed your database connection.
The simple solution is to make sure that the data you're binding to is not lazily loaded.
Create a List<T> or BindingList<T> object and pass the IList object from the query into the constructor. If the IList object is not a generic list, you can use LINQ, ilistObject.Cast<T>().ToList().
I'm sure this has been answered but I cannot find it...perhaps because the terms are used for so many differing questions...anyway.
how is the best way to store information about building dynamic controls on postback etc in the init event...
e.g. take the classic "questionnaire" scenario...question.aspx?id=1
get the id from the querystring
load the questions from where ever and build controls dynamically,
or dynamically add each question
user control for how ever many
questions etc
page posts back (user presses submit I guess!)
in the init event...where should you have already stored the id to
recreate the question controls?
things i've tried/thought of so far:
I initially stored the value in
viewstate as it's relevant to the
page, but the viewstate value isn't
available in page init (or is it??)
a hidden variable and then accessing the value in the form collection
session - I don't want to use session, seems problematic, e.g. what if the user views two questionnaires?
the querystring again? but that seems too easy to be changed and the dynamic controls just wouldn't make sense...
in the controls themselves? e.g. disregard the questionnaire id all together and simply go off the question id perhaps?? assuming that control ids would be in some format like controlQ# e.g. controlQ1, controlQ2. this means that each question id must be unique
any help/guidance etc much appreciated!!!
The query string will still be available during a postback, so you could take the ID from there.
If you are concerned about users manipulating the query string parameters, then maybe you should add some additional parameters which will allow you to verify the correctness of the query string during a postback. As a simple example, you could add a MD5 hash of the ID parameter and the current session's ID, and recreate/compare that checksum during a postback.
try to create/load the usercontrols in the page OnLoad.
if you want to load inside a usercontrol other usercontrols use the OnInit event.
use the viewstate for the persistence of the information you will need to re-instantiate the usercontrols during the postback. to map the postback data to the controltree you have to ensure that you have the same control-tree. after that mapping you could change your controltree (load some other controls e.g.)
contols-id: yes, you have to give them unique ids, otherwise it will not work. if you store your questions in a database, there should not be any problem to use the database-primary key for this, so name them e.g. question-1, question-2 (the controls)
Ok so the obvious answer is, because the flow of a composite control demands my childcontrols to be created at a certain point in time. I got a problem i think other people must have had as well.
My control is a composite "container/collection" control. It will be fed with an object and based on that objects data it will create a number of childcontrols. So my control will render a header (always) and x-number of, say TextBox controls (based on the object it was fed with).
I'm creating my header in CreateChildControls() obviously, but i can't possibly create my TextBoxes there as well, because i don't know if the object (to base the TextBoxes on) has been fed yet? I thought of exposing a property/method to set/fed the object through, but i'm not sure when it will be called.
So what do i do? I mean i can't possibly create the TextBoxes in CreateChildControls() or can I? I mean - when is CreateChildControls() called - i know i can call EnsureChildControls() (which i already do in a property to set the innerText of the header - since i need the header to be created before setting its innerText obviously).
How about this
var c = new MyContainerControl();
c.Header = "fun";
c.TextBoxObject = myTextBoxes;
That would raise an error (or at best not create any TextBox'es) if i put the building of the TextBoxes in CreateChildControls().
Would it be more sane to instead just store the Header in a member variable and thus not having to call EnsureChildControls() in the exposed method/property setting the Header innerText. I just don't like this aproach much, since it would be complicating things by adding extra logic to store temporarely and later having to figure out when to set it (probably in PreRender).
Also i guess i could make some kind of Databound control, ensuring the data be present at the time of calling .DataBind(). I really don't like this either since last i looked at creating databound controls it got very complicated.
This really should be an easy task to solve - I know I'm missing something somewhere...
what you're describing IS a databound control. And yes, it's somewhat complicated, but it's the proper design paradigm for this type of instance.
That said, had you considered utilizing the repeater control rather than trying to roll out your own composite which behaves in the exact same manner? Rather than passing it a random object, pass it a collection or an iList with the number of text areas you're wanting.
I have a problem with Gridview sorting that is similar to others but I'm binding to a collection object as opposed to a data table.
The existing business rules and data access layers of an application follow the pattern of having an object and, if you need a collection of objects of that type, to have another class inheriting CollectionBase and implementing IBindingList.
For desktop applications, it was easy to databind a gridview to one of these objects and there weren't any problems with turning on column sorting. Everything was 'in state' in the desktop app's presentation layer.
Now that code is being moved to a new web application (ASP.NET 2.0, VB codebehind pages).
I've played around with what I had to do to only have certain columns of the collection show up in the gridview and the gridview looked pretty good. When I turned on 'allow sorting', that's when the problems showed up.
I'm getting the error about not having a .Sorting method, etc. In researching this, I found all sorts of solutions that were easily implemented with dataviews if my source was a data table. But it's not - it's a collection. I tried to "cheap shot" a datasource by converting the collection to an XML memory stream and them trying to .ReadXML back into a dataset but that didn't work [Root element is missing error was as far as I got in the dataset.ReadXml(ioTemp) where ioTemp was the System.IO.MemoryStream used in the xml serializer].
Because of the old desktop apps, I've never had to worry about sorting a collection since the gridview handled it once it was loaded. In fact, it's a 'standard' that the collection's .SortProperty, .SortDirection and .ApplySort all through NotSupportedExceptions (I inherited this code from programmers long gone).
Is there an easy way to convert the collection to a data table or a way to sort the collection without having to go back to the database each time? Object Data Sources won't work becuase of the intricate rules in how the objects are built - the wizards in VS2005 just can't handle what we need to do (grabbing data from several tables conditionally to make an object).
Thanks in advance.
Have you considered client side sorting instead?
I have used the jquery tablesorter plugin in the past with ASP Gridviews.
http://tablesorter.com/
I had a similar issue and i needed to implement IComparable on the objects. Basically to sort a collection of objects you need a way to distinguish their order. The IComparable interface has one method called Compare which allows the .Net framework to work out the order of the objects when you sort them. You need to implement this method yourself to get the sort method to work.
Google results
You don't mention the error message so i cant be sure if this is the case, can you post the error?
EDIT :
In regards to your comment; you can implement multi column sorting, it just requires more work. You can specify the fields to sort the collection by and then use this information within the CompareTo Method.
Have a look at this
Given that you apparently are populating the grid with a collection of your own objects, this sounds like a perfect job for Linq for Objects. With just a little elbow grease you can achieve what is effectively an SQL Select statement against your collection. Very cool stuff.
http://www.hookedonlinq.com/LINQtoObjects5MinuteOverview.ashx
Also, do you really just want to sort the data in the grid? If so, then'd definitely pursue using Linq against your objects. However, rarely does sorting the contents of the grid really answer the problem ("sorting the grid" usually translates into changing the access path of the data used to fill the grid.) Browser apps aren't like Windows apps and don't have a full-time connection to the underlying data source to make things happen quite as magically as the DataGridView in Windows makes things seem.
You can put link buttons with an On_Click event as the header's of each column.
When the event is triggered, figure out which header was clicked on (one method per header or a commandArgument value). Once that is know, do a .orderBy or .OrderByDescending by on the collection of objects, and put the result back in as datasource of the gridview and databind on that.
In the year since I originally asked this question, I managed to get a new 'standard' implemented so that collections of business objects were now generic lists.
So now a "Collection class" that is little more than a "Inherits List(Of MyBusinessObject)" with a Sort Method that looks like this (performance wasn't an issue):
Public Overloads Sub Sort(ByVal strPropertyName As String, ByVal strDirection As String)
Dim arSortedList As New ArrayList
For Each item As MyBusinessObject In Me
arSortedList.Add(item)
Next
arSortedList.Sort(New CaseInsensitiveComparer(Of MyBusinessObject)(strPropertyName, strDirection))
For intI As Integer = 0 To arSortedList.Count - 1
Item(intI) = arSortedList(intI)
Next
End Sub
This seemed to work perfectly with the methodology used by the GridView for firing events.