[Coding] Cascading DropDownLists with jQuery/AJAX and WCF
Posted by Khatharsis on December 20, 2013
I try not to gripe too much about .NET, but I spent way too long trying to puzzle this out on my development machine. I don’t even want to think about the potential problems on production just yet. The basic premise: two DropDownLists, the second is dynamically populated based on a selection on the first. Using PostBack made this task quite trivial, but I’m not a fan of page reloads for this kind of thing, so I wanted to incorporate some AJAX. What a tangled mess it became.
Back in my WD days, we used the classic Web Service (.asmx) with the not-so-great XML/SOAP protocol. More recently, I came across the article of using WCF services (.svc) with JSON/REST protocol. Despite knowing I could easily output my own JSON in a separate file, in terms of potential growth of the app, it would be better to use a web service as a wrapper for the server-side part of AJAX calls. Otherwise, my project would be littered with files whose sole purpose is to output JSON.
I created an AJAX-enabled WCF Service file, which added some items to Web.config. I immediately compiled and ran the WCF service and got the message if I wanted to enable metadata, I had to fiddle with the Web.config settings. Needless to say, these instructions are much better than the one included on the .svc page. Mine ended up looking something like:
<system.serviceModel> <behaviors> <endpointBehaviors> <behavior name="Sandbox.WebServiceAspNetAjaxBehavior"> <enableWebScript /> </behavior> </endpointBehaviors> <serviceBehaviors> <behavior name="Sandbox.WebServiceAspNetAjaxBehaviors" > <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" /> <services> <service name="Sandbox.WebService" behaviorConfiguration="Sandbox.WebServiceAspNetAjaxBehaviors"> <endpoint address="" behaviorConfiguration="Sandbox.WebServiceAspNetAjaxBehavior" binding="webHttpBinding" contract="Sandbox.WebService" /> <endpoint contract="IMetadataExchange" binding="mexHttpBinding" address="mex" /> </service> </services> </system.serviceModel>
Where Sandbox is my namespace and WebService is the WCF service class I created. The “AspNetAjaxBehavior” appended to a few of the names were auto-generated when I created the class. The “AspNetAjaxBehaviors” is my own string based on the instructions from Microsoft. This substring can probably be something different.
From what I can tell, it is important to make sure the
Likewise, as far as I can tell, this just enables the generation of a WebService.cs class using the svcutil.exe tool provided with Visual Studio. I think this is mostly for console/forms applications and less relevant for web applications/AJAX calls (i.e., bears more research). Nonetheless, the option is set up and available if it becomes necessary later on.
Great. What’s next? The generated .svc file comes with a DoWork() method, which I changed:
[OperationContract]
[System.ServiceModel.Web.WebInvoke(Method = "GET",
ResponseFormat = System.ServiceModel.Web.WebMessageFormat.Json)]
public string DoWork()
{
return "Hello World";
}
The OperationContract decoration is necessary for all methods in the class. The WebInvoke method is not. The WebInvoke decoration is meant to build off of the REST concept (GET, POST, PUT, DELETE). A sibling decoration is WebGet for GET requests. WebInvoke handles the rest (POST, PUT, DELETE), although it can also handle GET. Many of the articles I looked at used WebInvoke with POST so that is what I ultimately went with for this proof of concept.
Before I go on to the next hurdle, one thing I ran across when using GET and POST (both with WebInvoke because I didn’t know any better at the time), was that I could see the results in the web browser. But as soon as I added in a second method that returned ListItem[] and tried to run this method in the browser, I was asked to save the file both for this method and DoWork(). This occurred in IE8, which is the target client browser, but FireFox still printed out the results as I’d expect.
Below is the code for my second method:
[OperationContract]
[System.ServiceModel.Web.WebInvoke(Method = "POST",
ResponseFormat = System.ServiceModel.Web.WebMessageFormat.Json)]
public ListItem[] GetDropDownListItems(int firstDdlID)
{
DataLayer db = new DataLayer();
return db.GetSecondDropDownListItems(firstDdlID);
}
DataLayer is a custom C# class I wrote to handle database transactions. GetSecondDropDownListItems() simply asks the database for value and text based on the first DropDownList selection. DataLayer then iterates down the list provided by the database and generates a ListItem[] which I simply return here. .NET handles the translation into JSON (there is no need to call .NET’s Serialize() method).
I also want to note that .NET adds a bunch of extra properties that are unnecessary, but since I am using .NET’s ListItem class, these are properties of the class and hence also get added into JSON. I could use a hash or dictionary for key-value pairs, but this method is also meant to be available to be called from the back-end code as well. So, I’ll take the extra properties even though I’m not using them. The resulting data should also remain small for the scope of this project, but if it becomes unwieldy, it is not difficult to change it.
Okay, so far so good. Now, I have to invoke the WCF service method via AJAX.
I went back and forth between GET and POST. jQuery’s AJAX type property must match the WebInvoke Method property or strange things happen. If .NET expects POST but receives an AJAX GET, I was given an empty array. If .NET expects GET but receives an AJAX POST, I got a “Method not allowed.” failure message.
I started off with GET, then switched to POST. I encountered an issue with POST that gave me the following error:
The server was unable to process the request due to an internal error. For more information about the error, either turn on IncludeExceptionDetailInFaults (either from ServiceBehaviorAttribute or from the
configuration behavior) on the server in order to send the exception information back to the client, or turn on tracing as per the Microsoft .NET Framework 3.0 SDK documentation and inspect the server trace logs.
I was already annoyed with the settings I changed. My mind was full of all the stuff I had done trying to remedy this and that. So, I went back to GET where this error didn’t appear. Unfortunately, I ran into some issue where jQuery’s AJAX data property was not being passed into the method. Or, it was being passed into the method (it got super difficult to debug the WCF service for some reason) but it wasn’t returning any data.
Eventually, I sucked it up and went back to figure out the ServiceBehaviorAttribute for a more detailed look at the problem. There are multiple approaches to solving this, but I found this class decoration:
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
Which gave me the following, more detailed error:
The incoming message has an unexpected message format ‘Raw’. The expected message formats for the operation are ‘Xml’, ‘Json’. This can be because a WebContentTypeMapper has not been configured on the binding. See the documentation of WebContentTypeMapper for more details.
Apparently it is a bad idea to have IncludeExceptionDetailInFaults set to true for production code and should only be set to true for debug mode, hence the original default message.
This message then led to the realization that I needed to set jQuery’s AJAX contentType property and everything finally worked. Below is my final AJAX code:
$('#DropDownList1').on('change', function () {
if ($('#DropDownList1').val() != "") {
var parameter = {};
parameter.firstDdlID = $('#DropDownList1').val();
$.ajax({
type: 'POST',
contentType: "application/json; charset=utf-8",
url: '<%=ResolveUrl("~/WebService.svc/GetDropDownListItems") %>',
data: JSON.stringify(parameter),
dataType: 'json',
processData: false,
success: populateDropDownList2,
error: function (a, b, c) {
alert(a.responseText);
}
});
}
else {
// Code here to set the second DropDownList to a default option
// and disable it
}
This took me a whole day. That’s just ridiculous. It irks me that .NET, for all it’s good parts, has some infuriatingly complex bad parts. It also seems like many of these bad parts are only fully understood by the .NET architects and users are left to flounder. Thankfully, there are the glorious things called the Internet, Google, and StackOverflow.
The next morning, I decided to re-tackle this problem. With a fresh mind, I wanted to better understand WCF, what I was working with and what the rules are. Since WCF follows REST, and I read many posts about GET being appropriate for retrieving data and POST being appropriate for inserting data, I tried to figure out why GET wasn’t working out for me.
Turns out it was due to a couple of things and there are actually a couple of ways to address them. The first method is not very pretty, but it can be more readable for users who aren’t familiar with jQuery’s AJAX. The code would look something like:
$('#DropDownList1').on('change', function () {
if ($('#DropDownList1').val() != "") {
$.ajax({
type: 'GET',
contentType: "application/json; charset=utf-8",
url: '<%=ResolveUrl("~/WebService.svc/GetDropDownListItems") %>' +
'firstDdlID=' + $('#DropDownList1').val(),
dataType: 'json',
success: populateDropDownList2,
error: function (a, b, c) {
alert(a.responseText);
}
});
}
else {
// Code here to set the second DropDownList to a default option
// and disable it
}
Note the change to the type property from POST to GET. I took out the parameter object and the corresponding data property. Instead, I added to the url string to make it look like a GET request you’d see in the browser. I also took out the processData property because it was unnecessary.
The second method is a little cleaner, but may be more confusing:
$('#DropDownList1').on('change', function () {
if ($('#DropDownList1').val() != "") {
var parameter = {};
parameter.firstDdlID = $('#DropDownList1').val();
$.ajax({
type: 'GET',
contentType: "application/json; charset=utf-8",
url: '<%=ResolveUrl("~/WebService.svc/GetDropDownListItems") %>',
data: parameter,
dataType: 'json',
success: populateDropDownList2,
error: function (a, b, c) {
alert(a.responseText);
}
});
}
else {
// Code here to set the second DropDownList to a default option
// and disable it
}
The only changes here from my original POST snippit is the type is set to GET and I removed the processData property. What processData does is turn the object set in the data property into a GET query string “fitting to the default content-type “application/x-www-form-urlencoded”” (from the jQuery API). processData is defaulted to true, so there is no need to declare it again.
The bonus of finally getting this to work (i.e., finding the correct combination of settings) is I can see the results in the browser much like I’d see the results of a classic Web Service in the browser. That brings me a certain peace of mind.