Introduction
When I was involved in one of my projects, we faced the problem of representing huge sets (more than 20000) of different entities in DropDownList
controls. If all items were bound to the control, the size of the page could be greater than 2-3 MB. Of course, it was not suitable, and the solution was found in adding a search feature to DropDownList
controls. This way, the user can specify the search criteria and decrease the set of items. And with this, some other issues arise as to how to decrease the number of postbacks and also make the search controls compact and comfortable. All the problems mentioned above are solved by using a combination of callbacks and JavaScript.
Background
When a control is bound to the datasource, it checks the number of objects in it. If the number of bound objects exceeds the defined limit, the control renders itself as shown on the first picture. When the user presses the button, the form for entering the search query appears near the drop down list. This form is shown on the second image. After the query is entered, the user should press the button, and the control will send a callback to the server. The result of this callback is the set of items that satisfies the specified search query. In this example, the user specifies the upper bound value and the DropDownList
is populated by integers that are lesser than that.
Implementation
The server control DDLWithSearch
is inherited from the standard System.Web.UI.WebControls.DropDownList
, and implements such interfaces as System.Web.UI.ICallbackEventHandler
and System.Web.UI.IPostBackDataHandler
. At the client side, it uses JavaScript functions defined in the DDLWithSearchCoreClass
. A JavaScript file is compiled to the control assembly as an embedded resource. This control defines some data manipulation events:
GetObjectsListCount
- is fired to retrieve the size of the items list that satisfies the search criteria.
GetObjectsList
- is fired to retrieve the list of items that satisfies the search criteria.
GetObjectByID
- is fired to retrieve the object to bound by its ID.
And the following properties:
MaxRenderItemsCount
- if the number of bound objects exceeds this limit, the control renders search controls.
MaxCallbackItemsCount
- the maximum number of items that can be returned as the callback result.
EnableSelectItem
- enables or disables the 'select...' item.
SelectItemText
- by default, this is set to 'select...'.
UseSearchItemText
- by default, this is set to 'please use search'.
NoItemsFoundItemText
- by default, this is set to 'no items found'.
TooBigResultItemText
- by default, this is set to 'too big result'.
ClientCallbackErrorHandler
- specifies the JavaScript function name that is called if errors during callback occurs.
EnableSearch
- enables or disables search controls.
As far as items can be changed on the client side, the control implements the System.Web.UI.IPostBackDataHandler
interface. Changes that are made with the options collection are stored in two hidden input elements that are registered in the OnPreRender
method. Also, it is in this method that the DDLWithSearch.js file and the callback hadlers are registered:
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (!this.Page.ClientScript.IsClientScriptBlockRegistered("DDLWithSearch"))
{
this.Page.ClientScript.RegisterClientScriptInclude(
this.GetType(), "DDLWithSearch",
Page.ClientScript.GetWebResourceUrl(this.GetType(),
"DDLWithSearch.DDLWithSearch.js"));
}
if ((_searchMode == SearchMode.SearchIsOn) || EnableAddItemsOnClient)
{
Page.ClientScript.RegisterHiddenField(string.Format("{0}{1}",
hiddenItemsPrefix, this.ClientID), string.Empty);
Page.ClientScript.RegisterHiddenField(string.Format("{0}{1}",
hiddenSelectedPrefix, this.ClientID), string.Empty);
string callBackRef = null;
if(ClientCallbackErrorHandler == string.Empty)
{
callBackRef = Page.ClientScript.GetCallbackEventReference(
this,
"document.getElementById('srchTextBox').value",
"DDLWithSearchCore.ReceiveDdlItems",
string.Format("'{0}'", this.ClientID)
);
}
else
{
callBackRef = Page.ClientScript.GetCallbackEventReference(
this,
"document.getElementById('srchTextBox').value",
"DDLWithSearchCore.ReceiveDdlItems",
string.Format("'{0}'", this.ClientID),
ClientCallbackErrorHandler,
false
);
}
this.Attributes.Add(attrRunSearchStatement,
string.Format("javascript:DDLWithSearchCore" +
".PrepareDdlToCallBack('{0}') && {1}",
this.ClientID, callBackRef));
}
}
The BindItems
method checks the number of bound objects, and if necessary, sets the SearchMode.SearchIsOn
mode:
public void BindItems()
{
int objectsCount = _GetObjectsListCount(string.Empty);
if (objectsCount > MaxRenderItemsCount &&
_searchSettings == SearchSettings.SearchEnabled)
{
_searchMode = SearchMode.SearchIsOn;
this.Items.Clear();
this.Items.Add(new ListItem(UseSearchItemText, this.NoneItemValue));
}
else
{
this.DataSource = _GetObjectsList(string.Empty);
this.DataBind();
if (EnableSelectItem)
{
this.Items.Insert(0, new ListItem(this.SelectItemText,
this.NoneItemValue));
}
}
}
The GetCallbackResult
methods gets and prepares the collection of items that satisfies the search query:
public string GetCallbackResult()
{
ClientItemsCollection clientItemsCollection = new ClientItemsCollection();
int objectsCount = _GetObjectsListCount(_callBackSearchQuery);
if (objectsCount > 0)
{
if (objectsCount < MaxCallbackItemsCount)
{
IEnumerator objectsListEnumerator =
_GetObjectsList(_callBackSearchQuery).GetEnumerator();
StringBuilder sb = new StringBuilder();
if (this.EnableSelectItem)
{
clientItemsCollection.AddItem(this.SelectItemText,
this.NoneItemValue);
}
while (objectsListEnumerator.MoveNext())
{
PropertyInfo piDataValue = null;
PropertyInfo piDataText = null;
if(piDataValue == null)
{
piDataValue =
objectsListEnumerator.Current.GetType().GetProperty(
this.DataValueField,
BindingFlags.Instance|BindingFlags.Public);
if (piDataValue == null)
throw new PropertyNotFoundExeption(this.DataValueField);
piDataText =
objectsListEnumerator.Current.GetType().GetProperty(
this.DataTextField, BindingFlags.Instance |
BindingFlags.Public);
if (piDataText == null)
throw new PropertyNotFoundExeption(this.DataTextField);
}
string dataValue =
piDataValue.GetValue(objectsListEnumerator.Current,
null).ToString();
string dataText =
piDataText.GetValue(objectsListEnumerator.Current,
null).ToString();
clientItemsCollection.AddItem(dataText, dataValue);
}
}
else
{
clientItemsCollection.AddItem(this.TooBigResultItemText,
this.NoneItemValue);
}
}
else
{
clientItemsCollection.AddItem(this.NoItemsFoundItemText,
this.NoneItemValue);
}
return clientItemsCollection.GetClientString();
}
Also, the SelectedValue
property requires some changes:
public new string SelectedValue
{
get
{
return base.SelectedValue;
}
set
{
ListItem item = Items.FindByValue(value);
if (item == null)
{
object newItemObject = _GetObjectByID(value);
if (newItemObject != null)
{
Type objType = newItemObject.GetType();
PropertyInfo textFieldProperty =
objType.GetProperty(this.DataTextField);
PropertyInfo valueFieldProperty =
objType.GetProperty(this.DataValueField);
string itemText = Convert.ToString(
textFieldProperty.GetValue(newItemObject, null));
string itemVal = Convert.ToString(
valueFieldProperty.GetValue(newItemObject, null));
this.Items.Add(new ListItem(itemText, itemVal));
}
else
{
throw new InvalidItemValueException();
}
}
base.SelectedValue = value;
}
}
Using the code
To use the control in your project, you should:
- Add a reference to the assembly DDLWithSearch.dll.
- Copy ddlWithSearch.css and the images folder from $ProjectDir$\Styles to the theme directory.
The DDLWithSearch.js file is compiled as an embedded resource and shouldn't be referenced manually.
To use this control on a page, you should:
- define event handlers for the events that it fires
- set the
DataTextField
and DataValueField
properties
This is an example of usage:
ddlTest.GetObjectsListCount += new
DDLWithSearch.DDLWithSearch.GetObjectsListCountEventHandler(
ddlTest_GetObjectsListCount);
ddlTest.GetObjectsList += new
DDLWithSearch.DDLWithSearch.GetObjectsListEventHandler(
ddlTest_GetObjectsList);
ddlTest.GetObjectByID += new
DDLWithSearch.DDLWithSearch.GetObjectByIdEventHandler(
ddlTest_GetObjectByID);
ddlTest.DataTextField = "TestText";
ddlTest.DataValueField = "TestValue";
ddlTest.BindItems();
The example of usage works fine in IE 6, Firefox 1.5, and Opera 8.54.
History
- 9 Aug 2006 - Article submitted.