Client Callbacks in Action Part 2 : Self-filling Drop Down List

On the first part of this two-post series I presented a text box that has the ability to suggest values, after a number of characters has been entered. It does this by leveraging the client callbacks functionality of ASP.NET. This time, I am going to show a drop down list that exposes a JavaScript function (fill) that receives a value, and will pass that value to a server-side event also through a client callback where we have the option to populate the drop down.

First, the target markup:

   1: <my:AutoFillDropDownList runat="server" ID="list" OnAutoFill="list_AutoFill" />

No custom properties whatsoever, just an handler for the AutoFill event, which will be called whenever the fill function is invoked.

The AutoFillDropDownList inherits from DropDownList:

   1: public class AutoFillDropDownList : DropDownList, ICallbackEventHandler
   2: {
   3:     public event EventHandler<AutoFillEventArgs> AutoFill;
   4:  
   5:     protected override void OnInit(EventArgs e)
   6:     {
   7:         this.Page.ClientScript.RegisterStartupScript(this.GetType(), this.UniqueID + "onFillCallback", String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ document.getElementById('{0}').onFillCallback = function(result, context) {{ document.getElementById('{0}').options.length = 0; var r = result.split('\\n'); for (var i = 0; i < r.length; ++i) {{ var keyValue = r[i].split('='); if (keyValue.length == 1) {{ continue }}; var option = document.createElement('option'); option.value = keyValue[0]; option.text = keyValue[1]; document.getElementById('{0}').options.add(option);  }} }} }});", this.ClientID), true);
   8:         this.Page.ClientScript.RegisterStartupScript(this.GetType(), this.UniqueID + "fill", String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ document.getElementById('{0}').fill = function(value) {{ document.getElementById('{0}_HiddenField').value = value; {1}; }} }});\n", this.ClientID, String.Format(this.Page.ClientScript.GetCallbackEventReference(this, "value", "document.getElementById('{0}').onFillCallback", null, true), this.ClientID)), true);
   9:         this.Page.ClientScript.RegisterHiddenField(String.Concat(this.ID, "_HiddenField"), this.Context.Request.Form[String.Concat(this.ID, "_HiddenField")]);
  10:  
  11:         this.Page.PreLoad += this.OnPreLoad;
  12:  
  13:         base.OnInit(e);
  14:     }
  15:  
  16:     public override void Dispose()
  17:     {
  18:         this.Page.PreLoad -= this.OnPreLoad;
  19:  
  20:         base.Dispose();
  21:     }
  22:  
  23:     protected void OnPreLoad(object sender, EventArgs e)
  24:     {
  25:         if (this.Page.IsPostBack == true)
  26:         {
  27:             var fillValue = this.Context.Request.Form[String.Concat(this.UniqueID, "_HiddenField")];
  28:  
  29:             if (String.IsNullOrWhiteSpace(fillValue) == false)
  30:             {
  31:                 var args = new AutoFillEventArgs(fillValue);
  32:                 
  33:                 this.OnAutoFill(args);
  34:  
  35:                 foreach (var key in args.Results.Keys.OfType<String>())
  36:                 {
  37:                     this.Items.Add(new ListItem(args.Results[key], key));
  38:                 }
  39:  
  40:                 var selectedValue = this.Context.Request.Form[this.UniqueID];
  41:  
  42:                 this.SelectedIndex = this.Items.IndexOf(this.Items.FindByValue(selectedValue));
  43:             }
  44:         }
  45:     }
  46:     
  47:     protected virtual void OnAutoFill(AutoFillEventArgs e)
  48:     {
  49:         var handler = this.AutoFill;
  50:  
  51:         if (handler != null)
  52:         {
  53:             handler(this, e);
  54:         }
  55:     }
  56:  
  57:     #region ICallbackEventHandler Members
  58:  
  59:     String ICallbackEventHandler.GetCallbackResult()
  60:     {
  61:         var output = this.Context.Items["Results"] as StringDictionary;
  62:  
  63:         return (String.Join(Environment.NewLine, output.Keys.OfType<String>().Select(x => String.Concat(x, "=", output[x]))));
  64:     }
  65:  
  66:     void ICallbackEventHandler.RaiseCallbackEvent(String eventArgument)
  67:     {
  68:         var args = new AutoFillEventArgs(eventArgument);
  69:  
  70:         this.OnAutoFill(args);
  71:  
  72:         this.Context.Items["Results"] = args.Results;
  73:     }
  74:  
  75:     #endregion
  76: }

Again, for client callbacks, we need to implement ICallbackEventHandler. In its RaiseCallbackEvent we raise the AutoFill event, store its output in the request itself (HttpContext.Items collection), and on GetCallbackResult, we take this value and pass it to the client.

Another important thing is, whenever there is a postback, we need to raise the AutoFill event again, because the drop down must be in a consistent state, therefore we need to get its items. We do that in the PreLoad event of the page.

The AutoFill event uses a special argument:

   1: [Serializable]
   2: public sealed class AutoFillEventArgs : EventArgs
   3: {
   4:     public AutoFillEventArgs(String parameter)
   5:     {
   6:         this.Parameter = parameter;
   7:         this.Results = new StringDictionary();
   8:     }
   9:  
  10:     public String Parameter { get; private set; }
  11:  
  12:     public StringDictionary Results { get; private set; }
  13: }

Two properties:

  • Parameter: the read-only value passed to the fill JavaScript function;
  • Results: the key-value pairs that will be used to fill the drop down list items.

A typical handler might look like this:

   1: protected void list_AutoFill(object sender, AutoFillEventArgs e)
   2: {
   3:     //just add 10 items
   4:     for (var i = 0; i < 10; ++i)
   5:     {
   6:         e.Results.Add(i.ToString(), e.Parameter + i.ToString());
   7:     }
   8: }

In order to invoke the filling, just call fill on JavaScript:

   1: document.getElementById('list').fill('something');

There are no dependencies on any external libraries other than Microsoft AJAX Library, which is included by the ScriptManager.

Hope you like it!

                             

No Comments