Wow! It’s been waaaaay too long since my last installment – been busy with latest programming contract, not that you care.

Since beating the proverbial dead horse is my specialty, this brief post is going to cover making a call to the Yahoo! Maps API via an ASP.NET web service (.asmx) which we then call from client script with the aid of the Microsoft ASP.NET AJAX library(ies).

In order to access the necessary JavaScript script libraries, we must first include the customary ScriptManager object in our page:

<asp:ScriptManager runat="server" ID="sm1">
    <Services>
        <asp:ServiceReference Path="~/ZipCodeService.asmx" />
    </Services>
</asp:ScriptManager>

As you may have already guessed, this declaration creates a ScriptManager for our page and includes a reference to the web service (which we will create momentarily) used for fetching the City and State upon user input of a US Zip Code.

Without further ado, here’s the complete web service code:

using System.Linq;
using System.Web.Script.Services;
using System.Web.Services;
using System.Xml.Linq;

namespace YahooMapsZipCodeLookup
{
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    [System.Web.Script.Services.ScriptService]
    public class ZipCodeService : System.Web.Services.WebService
    {
        private static readonly string MAP_URL = "http://local.yahooapis.com/MapsService/V1/geocode?appid=";
        private static readonly string MAP_APP_ID = "<YOUR ID HERE>";

        [WebMethod]
        public CityState GetCityStateByZip(string zipCode)
        {
            XDocument xdoc = XDocument.Load(MAP_URL + MAP_APP_ID + "&zip=" + zipCode);

            var state = root.Descendants().Where(el => el.Name.LocalName == "City").Single().Value;
            var city = root.Descendants().Where(el => el.Name.LocalName == "State").Single().Value;

            return new CityState { City = city; State = state; };
        }
    }
    public class CityState
    {
        public string City { get; set; }
        public string State { get; set; }
    }
}

After our ScriptManager registers this service, we now have access to proxy(ies) that can be called from the client side. To do so, we can create a wrapper to the service call:

function getCityStateByZip(zip) {
    YahooMapsZipCodeLookup.ZipCodeService.GetCityStateByZip(
                    zip,
                    onZipLookupSuccess,
                    onZipLookupError,
                    "<%= DateTime.Now %>"  // Passing as the context
         );
}

It’s not as simple as just including the above call – we need to provide the methods named in the second and third parameters (the two lines highlighted in green above). Which would look something like:

function getCityStateByZip(zip) {
    YahooMapsZipCodeLookup.ZipCodeService.GetCityStateByZip(
                    zip,
                    onZipLookupSuccess,
                    onZipLookupError,
                    "<%= DateTime.Now %>"  // Passing as the context
         );
}
function onZipLookupSuccess(result, context, methodName) {
    // Note how we can access the properties of the returned CityState object
    // The ScriptManager takes care of all of this for us!
    $get("CityStateResult").innerHTML = result.City + ", " + result.State;
}
function onZipLookupError(error, context, methodName) {
    // The error object's most interesting method is probably get_message(), which
    // will, in this case, return the message of any ASP.NET exceptions that may
    // have occured on the server side webservice.
    $get("CityStateResult").innerHTML = "Error: " + error.get_message();
}

Though we aren’t making any use of the context and methodName parameters being passed to our callback functions, I’ll just point out that the context is whatever was set when calling the webservice – in this case, the date and time were used. The context can be very useful to track asynchronous calls in a stateless environment.

The methodName, cleverly enough, returns the name of the web service method that we’ve called – in this case, methodName contains GetCityStateByZip which, as we all know, is the name of the only method exposed by our YahooMapsZipCodeLookup.ZipCodeService.

There is another option to writing explicit callback methods, and one that I prefer (probably due to my experience with jQuery). If we don’t want to write explicit methods, we can use anonymous functions as parameters in the call to our web service:

function getCityStateByZip(zip) {
    YahooMapsZipCodeLookup.ZipCodeService.GetCityStateByZip(
                zip,
                function(result, context, methodName) {
                    $get("CityStateResult").innerHTML =
                            result.City + ", " + result.State + "<br />"
                          + "context: " + context + "<br />"
                          + "methodName: " + methodName;
                },
                function(error, context, methodName) {
                    $get("CityStateResult").innerHTML =
                        "error: " + error.get_message();
                },
                "<%= DateTime.Now %>"  // Passing as the context
        );
}

The two highlighted lines indicate where we’ve replace the names of functions with anonymous functions. NB: the line numbers in the above snippet are supposed to correspond with the actual aspx page.

To hook everything up on our actual page, we need to add an event handler to, in this case, a textbox:

<input type="text" onchange="getCityStateByZip(this.value);" />
<input type="submit" value="Get City and State" onclick="return false;" />
<div id="CityStateResult"></div>

I also snuck in the CityStateResult div that is the target of our DOM manipulation upon receipt of a response from the web service. I also added a do-nothing submit button to demonstrate that a button press will fire the textbox’s onchange event.

The entire aspx page:

<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Calling a Web Service from ASP.NET AJAX Client Script</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <asp:ScriptManager runat="server" ID="sm1">
        <Services>
            <asp:ServiceReference Path="~/ZipCodeService.asmx" />
        </Services>
    </asp:ScriptManager>
    <input type="text" name="ziptxt" id="ziptxt" onchange="getCityState(this.value);" />
    <input type="submit" value="Get City and State" onclick="return false;" />
    <div id="cityStateResult"></div>
    </div>
    <script type="text/javascript">
        function getCityState(zip) {
            var citystate =
                YahooMapsZipCodeLookup.ZipCodeService.GetCityStateByZip(
                    zip,
                    function(result, context, methodName) {
                        $get("cityStateResult").innerHTML = 
                            result.City + ", " + result.State + "<br />"
                         + "context: " + context + "<br />"
                         + "method name: " + methodName;
                    },
                    function(error, context, methodName) {
                        $get("cityStateResult").innerHTML =
                            "error: " + error.get_message();
                    },
                    "<%= DateTime.Now %>"  // Passing as the context
                 );
        }
    </script>
    </form>
</body>
</html>

In a nutshell, when the user triggers the onchange event of our textbox, the event is handled by our wrapper function getCityStateByZip(zip), to which we pass this.value, which contains the changed value of the textbox. This value is then passed to the web service, which returns a CityState object – ASP.NET AJAX handles all of the serialization and other such details for us in a nearly transparent way. We can refer to the CityState object returned by our web service as if the object was a normal .NET object, i.e. result.City or result.State.

And I think that covers it! No more posts on zip code lookups, I promise!

Before I forget, here is the source to the samples seen above: Calling a Web Service with ASP.NET AJAX Client Script Source Code