locked
Cascading DropDownList using Web forms / jQuery / Ajax RRS feed

  • Question

  • User742805821 posted

    This wasted so much of my time for something so simple that I have to put a note here - just in case I need to do it again!

    Some tiny trip-up details in OfficeAjax() were:

    a. My website is configured as a Virtual so the web service call was "/Service/Offices.asmx/GetOffices2". You may need to use "../Service/Offices.asmx/GetOffices2"
    b. Attempting to use jQuery to add the "change" method for the DropDownList gave me an incomprehensible error. I gave up and used .Attributes.Add() on the server instead.
    c. It seems to prefer you to use a "POST" rather than a "GET"
    d. You must send a parameter to the web service even if it is just: data: {} !!!
    e. I thought that the object returned would be accessible as data so that data.length would give the number of items. No chance. The object returned is accessible as data.d !!!! so that data.d.length gives the number of items. [Note don't ask me where the .d. came from]
    f. See below OfficeAjax() for more details.
    g. Incomprehensible error messages back when the webservice or javascript does not work. Using IE 8 is useful here as it as a javascript debugger.

    1) Create a web service with a web method to send your data back as JSON. Something like this:

    /* ============ Offices.asmx Web Service ============
     Note: the class method ConsultantOffices.GetOffices() returns a List<ConsultantOffice>
    */
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Services;
    using MySite.Model;
    using MySite.Data;
    
    namespace MySite.Client.Service
    {
      [WebService(Namespace = "http://mysite.com/Service/")]
      [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
      [System.ComponentModel.ToolboxItem(false)]
      [System.Web.Script.Services.ScriptService]
      public class Offices : System.Web.Services.WebService
      {
        [WebMethod]
        public OfficeUpdater[] GetOffices2(string consultantID)
        {
          var officesBack = from office in ConsultantOffices.GetOffices(Convert.ToInt32(consultantID))
                              select new OfficeUpdater(office.ConsultantOfficeID, office.OfficeName);
          return officesBack.ToArray<OfficeUpdater><OFFICEUPDATER>();
        }
      }
    }
    
    /* ============ POCO to send Office data back via the Web Service ============ */
    [Serializable]
    public class OfficeUpdater
    {
      public int OID { get; set; }
      public string OName { get; set; }
    
      public OfficeUpdater() { }
    
      public OfficeUpdater(int id, string name)
      {
        OID = id;
        OName = name;
      }
    }
    2) You need two DropDownLists on your web form. I think I could make EnableViewState="false" without any problems here. It would save time and bandwidth.
      <div>Consultant</div>
      <asp:DropDownList ID="selConsultant" runat="server" EnableViewState="true"
    	DataValueField="ConsultantID" DataTextField="ConsultantName" />
      
      <span id="spnOfficeMsg">Optionally by office:</span>
      <asp:DropDownList id="selOffice" runat="server" EnableViewState="true"
    	DataValueField="ConsultantOfficeID" DataTextField="OfficeName" />
    3) Data is initially bound to the forms in code behind (in this example).
    protected void Page_Init(object sender, EventArgs e)
    {
      if (!Page.IsPostBack)
      {
         selConsultant.DataSource = Consultants.GetConsultants_For_DDL();
         selConsultant.DataBind();
         selConsultant.Items.Insert(0, new ListItem("All", "0"));
    
         selOffice.DataSource = ConsultantOffices.GetOffices(SearchState.ConsultantID);
         selOffice.DataBind();
         selOffice.Items.Insert(0, new ListItem("All", "0"));
      }
    }
    4) I added a client-side event to the first DropDownList and wrote some JavaScript to tell the client what the ids of the two DropDownLists were.

     

    protected void Page_Load(object sender, EventArgs e)
    {
      selConsultant.Attributes.Add("onchange", "OfficeAjax();");
      Page.ClientScript.RegisterClientScriptBlock(typeof(Page), "Search", WriteJavaScript());
    }
     /* The above just produced javascript which looks something like this (for those of you using MasterPages):
    <script defer="defer" type="text/javascript">
      //<![CDATA[
      var selConsultantID = 'ctl00_cph1_selConsultant';
      var selOfficeID = 'ctl00_cph1_selOffice';
      //]]>
    </script>
    */

    5) This is the jQuery code for the Cascading dropdown list:

    <script src="/javascript/jquery-1.3.2.min.js" type="text/javascript" defer="defer"></script>
    <script type="text/javascript">
     //&lt;![CDATA[
      function OfficeAjax() {
        $.ajax({
          type: "POST",
          contentType: "application/json; charset=utf-8",
          url: "/Service/Offices.asmx/GetOffices2",
          data: "{consultantID:" + $("#" + selConsultantID + " > option:selected").attr('value') + "}",
          dataType: "json",
          success: function(data) { OfficeUpdating(data.d); },
          error: function(XMLHttpRequest, textStatus, errorThrown) { alert(textStatus + " " + errorThrown); }
        });
      } 
      function OfficeUpdating(items) {
        $('#' + selOfficeID + ' > option').remove();
        if (items.length > 0) {
          var options = '';
          for (o in items) {
            var office = items[o];
            options += "&lt;option value='" + office.OID + "'>" + office.OName + "&lt;/option>";
          }
          $("#" + selOfficeID).removeAttr('disabled').html(options);
        } else {
          OfficedNotChanged();
        }
      }
    
      function OfficedNotChanged() {
        $("#" + selOfficeID).attr('disabled', true).html('');
        $("#" + selOfficeID).append("&lt;option value='0'>(None Found)</option>");
      }
    //]]>
     </script> 
    Monday, May 18, 2009 3:36 PM

All replies

  • User742805821 posted

    The problem with the method above is that is doesn't work out of the box. On the production server, when one tries to access the web service one finds that the necessary permissions are not there.

    You could start messing with directory permissions but a better solution is to use either:

    a. asp.net ajax web service in combination with jQuery OR
    b. page methods.

    My solution using page methods is:

    1. Drop the web service used above.
    2. Add a static method in the code behind of the page:

        [WebMethod]
        public static OfficeUpdater[] GetOffices(string consultantID)
        {
          var officesBack = from office in ConsultantOffices.GetOffices(Convert.ToInt32(consultantID))
                            select new OfficeUpdater(office.ConsultantOfficeID, office.OfficeName, office.Default);
          return officesBack.ToArray<OfficeUpdater>();
        }

    This needed two directives:
    using System.Linq;
    using System.Web.Services;

    3. Set EnablePageMethods="true" in your page's ScriptManager:

    <asp:ScriptManager ID="ScriptManager1" runat="Server" EnablePageMethods="true" />

    4. Some of the javascript I wrote is still OK. The new javascript is:

    <script src="/javascript/jquery-1.3.2.min.js" type="text/javascript" defer="defer"></script>
    <script type="text/javascript" defer="defer">
      //<![CDATA[
      function OfficeAjax() {
        var consultantID = $("#" + selConsultantID + " > option:selected").attr('value');
        PageMethods.GetOffices(consultantID, OnSucceeded, OnFailed);
      }

      function OnSucceeded(result, userContext, methodName) {
        OfficeUpdating(result);
      }

      function OnFailed(error, userContext, methodName) {
        $('#' + selOfficeID + ' > option').remove();
        OfficedNotChanged();
      }

      function OfficeUpdating(items) {
        $('#' + selOfficeID + ' > option').remove();
        if (items.length > 0) {
          var options = '';
          for (o in items) {
            var office = items[o];
            options += "<option value='" + office.OID + "'>" + office.OName + "</option>";
          }
          $("#" + selOfficeID).removeAttr('disabled').html(options);
        } else {
          OfficedNotChanged();
        }
      }

      function OfficedNotChanged() {
        $("#" + selOfficeID).attr('disabled', true).html('');
        $("#" + selOfficeID).append("<option value='0'>(None Found)</option>");
      }
    //]]>
    </script>

    5. The rest of the code from the code behind in the OP stays.

    Wednesday, May 20, 2009 6:57 AM