Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

DropDownList control with items search, using callback

3.93/5 (6 votes)
17 Aug 20063 min read 3   703  
Extended DropDownList control, that allows searching items without postback.

DDL with search

Search query form

Search result

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 search button 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 go button 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:

C#
protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);

    if (!this.Page.ClientScript.IsClientScriptBlockRegistered("DDLWithSearch"))
    {
        // Define the resource name and type.
        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)
        {
            // Without Error handle
            callBackRef = Page.ClientScript.GetCallbackEventReference(
                            // control
                            this,
                            // argument - the value of the txtSearch
                            "document.getElementById('srchTextBox').value",
                            // client call back function
                            "DDLWithSearchCore.ReceiveDdlItems",
                            // context - ddl id
                            string.Format("'{0}'", this.ClientID)
                            );
        }
        else
        {                
            //With Error handle
            callBackRef = Page.ClientScript.GetCallbackEventReference(
                                // control
                                this,
                                // argument - the value of the txtSearch
                                "document.getElementById('srchTextBox').value",
                                // client call back function
                                "DDLWithSearchCore.ReceiveDdlItems",
                                // context - ddl id
                                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:

C#
public void BindItems()
{
    // get the number of objects
    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:

C#
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:

C#
public new string SelectedValue
{
    get
    {
        return base.SelectedValue;
    }
    set
    {
        // Check if there is already such item in list
        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:

C#
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here