WCF, AJAX, and Virtual Earth in Visual Studio 2008

Eric Ebell, working at one of our service provider customers in Atlanta, pinged me about a JavaScript question, wanting to know how to call a web service and then fill a table dynamically with the results.  I am not a JavaScript developer by any means, so this was a cool little challenge to take Visual Studio 2008 and .NET 3.5 for a drive to see how it holds up for a non-JavaScript developer.  This example will use the Virtual Earth SDK to locate an address and then add pushpins around that area.  The pushpin data is delivered via a WCF service, and we will use the AJAX support in ASP.NET 3.5 to make it a little easier to add event handling.

One of the cool things about ASP.NET and WCF in .NET 3.5 is the ability to use the ScriptManager control to generate proxies to services that return JSON instead of angle brackets.  Here's a short example that references the Virtual Earth script, our custom JavaScript file, and also references a WCF service.

 <%@ Page Language="C#" AutoEventWireup="true" %>
 
<html xmlns="https://www.w3.org/1999/xhtml" >

    <head id="Head1" runat="server">
        <title>Restaurants Near the Las Colinas Microsoft Office</title>
    </head>
 
    <body onload="LoadMap();">

        <form id="form1" runat="server">
             
            <asp:ScriptManager ID="ScriptManager1" runat="server">
                <Scripts>
                    <asp:ScriptReference Path="https://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=5" />
                    <asp:ScriptReference Path="mapscripts.js" />
                </Scripts>
                <Services>
                    <asp:ServiceReference Path="Service.svc"  />
                </Services>
            </asp:ScriptManager>
     
     
            <div style="width: 600px; height: 400px; position: relative; overflow:hidden;" id="VEMap"></div>
     
            <table style="width: 100%;" id="PointTable" border="1">
                <tr>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Description</th>
                    <th>Latitude</th>
                    <th>Longitude</th>
                </tr>
            </table>
        </form>
    </body> 
</html>

The UI is really clean and easy to comprehend.  Now that we defined the UI, let's look at our WCF service.  It's very simple, we just cons up a few Location objects and return them.  Note the object initialization shorthand that .NET 3.5 provides.

 using System;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Collections.Generic;

[ServiceContract(Namespace="DPE.Samples")]
[AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]
public class Service
{
    [OperationContract]
    public List<Location> GetLocations()
    {
        List<Location> points = new List<Location>();
        points.Add(new Location { Name = "Microsoft Las Colinas", Description = "7000 State Highway 161", Latitude = 32.900417, Longitude = -96.964060 });
        points.Add(new Location { Name = "Mi Cocina", Description = "7750 N. MacArthur Blvd", Latitude = 32.912134, Longitude = -96.958569 });
        points.Add(new Location { Name = "On The Border", Description = "1220 Market Pl", Latitude = 32.918477, Longitude = -96.964419 });
        points.Add(new Location { Name = "Chili's", Description = "1300 Market Pl", Latitude = 32.918576, Longitude = -96.965323 });
        return points;
    }
}

[DataContract]
public class Location
{
    [DataMember] public string Name { get; set; }
    [DataMember] public string Description { get; set; }
    [DataMember] public double Latitude { get; set; }
    [DataMember] public double Longitude { get; set; }
}

Again, there's nothing really special about this service, other than we are using the .NET 3.5 enableWebScript behavior in the configuration, which Visual Studio 2008 automagically adds for us.

The final bit to finish up is that we need to write some JavaScript.  This is our custom MapScripts.js file that was referenced in the ScriptManager.

  
var map;
      
function LoadMap()
{    
    map = new VEMap('VEMap');
    map.LoadMap();    
    map.Find(
        null,
        '7000 State Highway 161, Irving, TX', 
        null, 
        null, 
        null,
        null,
        true,
        true,
        true,
        true,
        FindCallBack);
}
 
function FindCallBack(ShapeLayer, FindResult, Place, HasMore)
{
    if (Place != null)
    {
        if (Place[0] != null)
        {                  
            DPE.Samples.Service.GetLocations(WebServiceCallBack);            
        }
    }
}
 
function WebServiceCallBack(Response)
{     
    
    for(var i = 0;i< Response.length;i++)
    {
        AddPushPin(Response[i], i);
        AddRow(Response[i], i);        
               
    }                                        
}

function AddPushPin(point, index)
{
    var pin = new VEPushpin(
        index, 
        new VELatLong(point.Latitude, point.Longitude), 
        null, 
        point.Name, 
        point.Description);        
    map.AddPushpin(pin); 
}

function AddRow(point, index)
{
    var table = $get("PointTable"); 
    
    var row = table.insertRow(table.rows.length);
    var idCell = row.insertCell(0);
    idCell.appendChild(document.createTextNode(index));
    
    var nameCell = row.insertCell(1);
    nameCell.appendChild(document.createTextNode(point.Name));
    
    var descriptionCell = row.insertCell(2);
    descriptionCell.appendChild(document.createTextNode(point.Description));
    
    var latCell = row.insertCell(3);
    latCell.appendChild(document.createTextNode(point.Latitude));
    
    var longCell = row.insertCell(4);
    longCell.appendChild(document.createTextNode(point.Longitude));
    
    $addHandler(row,"click", RowClick);
    
}

function RowClick(e)
{
   //Center the map where the thing was just clicked   
   map.PanToLatLong(new VELatLong(e.target.parentElement.cells[3].innerText,e.target.parentElement.cells[4].innerText));
}

The JavaScript code is really simple.

  1. The LoadMap function is called because the Body element points to this method as a callback handler in its onload method.
  2. In the LoadMap function, we call Find on the VEMap object and provide a callback pointer to our FindCallBack method.
  3. In FindCallBack, we call our web service to get the list of Location objects.
  4. When the web service call returns, we dynamically add a row to the HTML table, add a click handler to the row, and add a pushpin to the map.
  5. The RowClick handler handles the onclick event for the HTML row, which pans the map to the lat/long that we specify.

The result is pretty cool. When you click one of the dynamically generated rows in the table, the map pans to the Virtual Earth pushpin point. WCF, AJAX, and Virtual Earth

I'll admit, there was a bit of coding, setting breakpoints, and inspecting various objects.  That's how I found the table.insertRow and row.insertCell methods, simply by inspecting the DOM using Visual Studio 2008.  I used the same approach to figure out what code to put in the RowClick method, figuring out how to grab the values from the currently clicked row. 

Oh yeah, the whole thing took me about 20 minutes, the majority of the time was spent trying to figure out how to add rows to the table.  I am loving Visual Studio 2008 right about now.