Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / XHTML

Lat Lays Flat - Part 3: Creating A Google Maps .NET Control

5.00/5 (12 votes)
17 Oct 200512 min read 5   2.8K  
Creating an ASP.NET server control wrapper for the Google Maps API.

Introduction

This is the third article in a three article series examining a custom ASP.NET server control I developed to make using the Google Maps API easier for .NET developers. This is a technical article which does not focus on the usage of the Google Maps .NET control. If you want to see what this baby can do, check out Part 1 and Part 2. This article assumes you are familiar with the Google Maps API. You may see references throughout the article to "my control, my GoogleMaps control, my GMap control, etc". I did not create the Google Maps API; I merely created a wrapper for ASP.NET using C#, XML and XSL.

The main goal of this article is to show you how I created the Google Maps .NET control; the design decisions I made, the technology used, and the tools involved.

Some of the topics covered include:

  • XmlSerialization – Converting objects to XML.
  • XSL/XSLT – Converting XML to JavaScript.
  • AJAX – Calling server-side code from the client.
  • ASP.NET server controls – Custom events, data binding, post back data handling, and rendering.

The server control

I spent a lot of time thoroughly documenting the code for GMap.cs. Rather than go through it line-by-line and re-type my documentation, I will highlight the portions that are the magic behind the GMap. The GMap control contains a lot of advanced material which can be difficult to conceptualize merely by reading. I encourage you to look at Part 2 of this article series as you are reading through this material. Part 1 and Part 2 are the "what" and Part 3 is the "how." There is a team of monkeys working around the clock trying to figure out the "why."

First things first

The first step in creating the GMap control was to re-create all of the Google Maps API JavaScript objects in C#. If you download the code (see Download source files above) and look in the WCPierce.Web project, drill down through the folders until you come to GoogleMap. In this folder you will see one code file for every Google Maps API object. GIcon, GMarker, GPoint, GSize, etc. Each one recreated to exactly mimic the functionality of their JavaScript counterpart. If you look closely at anyone of these objects, you'll notice they are decorated with System.Xml.Serialization attributes. Those will come in handy when we want to convert from C# to XML (more on this later). Those Rolla guys have a good article on XML Serialization if you want to learn more.

Why go through the trouble of re-creating the JavaScript functionality in C#? So that you can write clean looking Google Maps code in the .NET language of your choice. The sample below shows how to create some points and markers and add them to a map:

C#
GPoint gp = new GPoint(-122.141944F, 37.441944F);
GMarker gm = new GMarker(gp, "FirstMarker");
gMap.Overlays.Add(gm);
gm = new GMarker(new GPoint(gp.X + 0.005F, 
                    gp.Y + 0.005F), "SecondMarker");
gMap.Overlays.Add(gm);
gMap.CenterAndZoom(gp, 4);

Public methods

Each of the public methods from the Google Maps API is re-created in the GMap control. Each method takes the provided parameters and creates the JavaScript code required to execute the method on client-side. This may seem a little odd: server-side code is creating client side code. But the real goal is for the code to be executed on client side. The examples below show the creation of the proper JavaScript:

C#
public string CenterAndZoom( GPoint LatLng, int ZoomLevel )
{
  string str = String.Format(Utilities.UsCulture, 
            "{0}.centerAndZoom(new GPoint({1}, {2}), {3});", 
            this.JsId, LatLng.X, LatLng.Y, ZoomLevel);
  initJs.Append(str);
  return str;
}

public string ZoomTo(int ZoomLevel )
{
  string str = String.Format(Utilities.UsCulture, 
              "{0}.zoomTo({1});", this.JsId, ZoomLevel);
  initJs.Append(str);
  return str;
}

The control's JavaScript ID, this.JsId, is created by the control to be unique. This allows multiple GMap controls to be manipulated on a page. The generated JavaScript code is added to the StringBuilder initJs and it is also the return value of each method call. This allows the developer to use the control to initialize a GMap (in a Page_Load handler for example) as well as make method calls during a client callback (more on this later).

CreateChildControls()

In the CreateChildControls() method we add four HtmlInputHidden controls:

C#
protected override void CreateChildControls()
{   
  base.CreateChildControls();

  string id = this.ID;

  HtmlInputHidden centerLatLng = new HtmlInputHidden();
  centerLatLng.ID = id + _centerLatLngField;
  base.Controls.Add(centerLatLng);

  HtmlInputHidden spanLatLng = new HtmlInputHidden();
  spanLatLng.ID = id + _spanLatLngField;
  base.Controls.Add(spanLatLng);

  HtmlInputHidden boundsLatLng = new HtmlInputHidden();
  boundsLatLng.ID = id + _boundsLatLngField;
  base.Controls.Add(boundsLatLng);

  HtmlInputHidden zoomLevel = new HtmlInputHidden();
  zoomLevel.ID = id + _zoomLevelField;
  base.Controls.Add(zoomLevel);
}

These controls allow the GMap control to capture the CenterLatLng, SpanLatLng, BoundsLatLng, and ZoomLevel values from a GMap during a postback. For example, let's say you added a GMap to your page and an asp:button. The user then moves the map around, perhaps zooms in/out, and then clicks your button. In the event handler for your button you could retrieve the center coordinate of the map and the current zoom level of the map based on its position when the user clicked the button.

OnDataBinding()

The GMap control allows developers to bind data from various sources (custom business objects, ArrayList, DataSet, etc.) to automatically create markers on the map. The developer specifies the following fields to implement databinding: DataLatitudeField, DataLongitudeField, DataMarkerIdField, and DataIconIdField. Latitude and Longitude fields are required. If the developer does not specify the MarkerId and IconId fields, markers are created with no ID and the default (red) icon. Developers can now easily add markers to a map using code like this:

C#
DataSet ds = GetCounties();
gMap.DataSource = ds;
gMap.DataMarkerIdField  = "CountyName";
gMap.DataLongitudeField = "Longitude";
gMap.DataLatitudeField  = "Latitude";
gMap.DataBind();

OnDataBinding() takes the bound DataSource and iterates through the records looking for the fields specified and creating a GMarker for every item.

Render()

Render() is usually the busy method for complex server controls (like the GMap). However, I decided to do something clever. All of the objects that make up the server-side GMap (GIcons, GMarkers, GPoints, GPolyines, etc.) are converted to XML using standard .NET XML serialization. Remember the serialization attributes I pointed out at the beginning of the article? That combined with the built in functionality of the framework allows us to easily generate XML from .NET objects.

C#
protected override void Render(HtmlTextWriter output)
{
  base.Render(output);     
  StringBuilder sb = new StringBuilder();
  string path = Page.Server.MapPath(this.ScriptFolderPath + 
                                            "/" + "GMap.xsl");
  XsltArgumentList xal = GetXsltArguments();
  sb.Append(GXslt.Transform(_gxpage, path, xal));
  Page.RegisterStartupScript(this.UniqueID, sb.ToString());  
}

RaisePostBackEvent()

I created the GMap to support five events: Click, Zoom, Move Start, Move End, and Marker Click. These are server-side events that respond to client callbacks. If you're a little hazy on client callbacks, check out my article series AJAX Was Here. What it all boils down to is running server-side code as a result of client side events. With some custom JavaScript, we send the GMap control the event we would like to be raised along with any event arguments. RaisePostBackEvent() parses the event request and raises the proper server-side event which then runs the developer's code.

C#
public void RaisePostBackEvent(string eventArgument)
{
  // If this isn't a call back we won't bother
  if( !CallBackHelper.IsCallBack )
    return;

  // Always wrap callback code in a try/catch block
  try
  {
    string[] ea = eventArgument.Split('|');
    string[] args = null;
    GPointEventArgs pea = null;
    string evt = ea[0];
    switch( evt )
    {
      // GMap Click event sends the coordinates 
      // of the click as event argument
      case "GMap_Click":
        args = ea[1].Split(',');
        pea = new GPointEventArgs(float.Parse(args[0]), 
                                float.Parse(args[1]), this);
        this.OnClick(pea);
        break;
      // GMarker Click event sends the coordinates of 
      // the click as event argument
      case "GMarker_Click":
        args = ea[1].Split(',');
        GPoint gp = new GPoint(float.Parse(args[0]), 
                                    float.Parse(args[1]));
        GMarker gm = new GMarker(gp, args[2]);
        pea = new GPointEventArgs(gp, gm);
        this.OnMarkerClick(pea);
        break;
      // GMap Move Start event sends the 
      // coordinates of the center of the 
      // map where the move started
      case "GMap_MoveStart":
        args = ea[1].Split(',');
        pea = new GPointEventArgs(float.Parse(args[0]), 
                                      float.Parse(args[1]));
        this.OnMoveStart(pea);
        break;
      // GMap Move End event sends the 
      // coordinates of the center of the 
      // map where the move ended
      case "GMap_MoveEnd":
        args = ea[1].Split(',');
        pea = new GPointEventArgs(float.Parse(args[0]), 
                                      float.Parse(args[1]));
        this.OnMoveEnd(pea);
        break;
      // GMap Zoom event sends the old and new zoom levels
      case "GMap_Zoom":
        args = ea[1].Split(',');
        GMapZoomEventArgs zea = 
                    new GMapZoomEventArgs(int.Parse(args[0]), 
                    int.Parse(args[1]));
        this.OnZoom(zea);
        break;
      // Default: we don't know what the 
      // client was trying to do
      default:
        CallBackHelper.Write(String.Empty);
        break;
    }
  }
  // Had some odd Thread Abort Exceptions 
  // going around. Something to do with 
  // the default behavior of Response.End. 
  // Just ignore these exceptions
  catch(Exception e)
  {
    if( !(e is System.Threading.ThreadAbortException) )
      CallBackHelper.HandleError(e);
  }
}

The developer can "hook up" with these events similar to the example below.

C#
private void Page_Load(object sender, System.EventArgs e)
{
  //. . .
  gMap.MarkerClick += 
           new GMapClickEventHandler(gMap_MarkerClick);
  //. . .
}

or

HTML
<wcp:GMap runat="server" id="gMap" Width="750px" Height="525px" 
  EnableClientCallBacks="True" OnMarkerClick="gMap_MarkerClick"/>
C#
protected string gMap_MarkerClick(object s, GPointEventArgs pea)
{
  //. . .
}

The results of each event are written back to the client. The results should be JavaScript code that the developer wants executed on client-side.

C#
protected virtual void OnMarkerClick(GPointEventArgs pea)
{
  GMapClickEventHandler eh = 
      (GMapClickEventHandler)base.Events[GMap.EventMarkerClick];

  if(eh != null) 
  { 
    CallBackHelper.Write(eh(pea.Target, pea));
  }
  else
  {
    CallBackHelper.Write(String.Empty);
  }
}

This can be really difficult to conceptualize so I encourage you to look at Part 2 of this article series as you are reading through this material.

LoadPostData()

The final point of interest in the GMap control is the LoadPostData() method. This method is called during a postback so that custom controls can retrieve data from the posted form. Here we are grabbing the values from the HtmlInputHidden controls we added during CreateChildControls(). We grab these values and save them to member variables so that developers can access when needed.

C#
public bool LoadPostData(string postDataKey, 
  System.Collections.Specialized.NameValueCollection postCollection)
{
  string uId = this.UniqueID;
  try { _centerLatLng = (GPoint)postCollection[uId + _centerLatLngField]; } 
  catch { }
  try { _spanLatLng   = (GSize)postCollection[uId + _spanLatLngField]; } 
  catch { }
  try { _boundsLatLng = (GBounds)postCollection[uId + _boundsLatLngField]; } 
  catch { }
  try { _zoomLevel = Convert.ToInt32(postCollection[uId + _zoomLevelField]); } 
  catch { }

  return false;
}

The XSL

Before I launch in to an explanation of GMap.xsl, I want to provide a few soap box remarks. Before working on the GMap, I hadn't used XSL. I thought it would be neat to try it out and decided to use it for a few reasons:

  • The Google Maps API is still evolving. If Google decided to make a change to the API, I needed an easy way to tweak the JavaScript generated by my control. XSL seemed like the answer.
  • I wanted to learn something new and see what the big deal was about XSL.
  • I wanted to attract a larger audience by promoting my article with the XSL buzz word.

Now that I have actually used XSL in a project I've come to this conclusion: XSL is the Devil. I wasn't sure if I was the only one with this sentiment so I did a little digging and came across this great article by Michael Leventhal titled XSL Considered Harmful. I thought Michael made some excellent points many of which I came across during my development. But considering the article was six years old and we still use XSL today, I guess the sadists have won out in preserving XSL as a viable development tool. But, time to come off the soap box.

Below is an example of the XML generated from a simple GMap:

XML
<?xml version="1.0" encoding="utf-16"?>
<GXPage xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Overlays>
    <GMarker Id="Motel 6 (#1054)" IconId="BlueMarker">
      <Point X="-122.029" Y="37.3953" />
    </GMarker>
  </Overlays>
  <Controls>
    <GSmallMapControl />
  </Controls>
  <Icons>
    <GIcon Id="BlueMarker" 
      Image="http://localhost/TestWeb/Advanced/blueMarker.png"
      Shadow="" PrintImage="" MozPrintImage="" PrintShadow="" 
      Transparent="">
      <ImageMap />
    </GIcon>
  </Icons>
</GXPage>

This XML snippet is not to be confused with the original/current XML that the Google Maps natively understands. The reason I decided to do my own thing is because there is no formal documentation for the Google Maps XML format and I didn't want to commit to something that may change in the near future. The goal now is to take the XML and transform it into the JavaScript necessary to create a Google Map. This is accomplished by the XSL stylesheet I created named GMap.xsl.

Some of the data used to create the Google Map is not found in the XML file. A number of values are passed into the XSL stylesheet as parameters. The XML only contains map data like markers, icons, polylines, controls, etc. GMap.xsl starts off by specifying an output of text (rather than HTML) and declaring a number of parameters that should be passed to the stylesheet. The values can be accessed by developers using the GetXsltArguments() method of a GMap.

XML
<xsl:stylesheet version="1.0" 
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" />
  <xsl:param name="jsId" />
  <xsl:param name="divId" />
  <xsl:param name="controlId" />
  <xsl:param name="enableClientCallBacks" />
  <xsl:param name="friendlyControlId" />
  <xsl:param name="initJs" />
  <xsl:param name="enableDragging" />
  <xsl:param name="enableInfoWindow" />
  <xsl:param name="zoomLevel" />
  <xsl:param name="mapType" />

The next portion of the stylesheet takes the parameters provided and uses them to create and initialize a Google Map using the proper API values. Any methods called during the creation of the GMap (CenterAndZoom(), OpenInfoWindow(), etc.) are added to the InitJS member variable. InitJS contains the JavaScript required to make the client-side call to the desired Google API method (See Public methods above). The actual map initialization code is placed inside a method, which is later called after the window finishes loading. The reason for this is to avoid getting an "Operation Aborted" error in Internet Explorer.

XML
var <xsl:value-of select="$jsId" /> = null;
function <xsl:value-of select="$friendlyControlId" />_Render() {
  if( GBrowserIsCompatible()) {
    <xsl:value-of select="$jsId" /> =
                  new GMap( document.getElementById(
                        "<xsl:value-of select="$divId" />"));
    <xsl:value-of select="$jsId" />.id = '<xsl:value-of select="$controlId" />';
    <xsl:if test="$enableDragging=false()">
      <xsl:value-of select="$jsId" />.disableDragging();
    </xsl:if>
    <xsl:if test="$enableInfoWindow=false()">
      <xsl:value-of select="$jsId" />.disableInfoWindow();
    </xsl:if>
    <xsl:value-of select="$jsId" />.zoomTo(
                    <xsl:value-of select="$zoomLevel" />);
    <xsl:value-of select="$jsId" />.setMapType(
                      <xsl:value-of select="$mapType" />);
    <xsl:value-of select="$initJs" />

If the developer enabled client call backs on the GMap control, the five server side callback events Click, Marker Click, Zoom, Move Start, and Move End are attached to the GMap. These events are named GMap_Server<Event Name> (more on this in The JavaScript section below).

XML
<xsl:if test="$enableClientCallBacks=true()">
  GEvent.addListener(<xsl:value-of select="$jsId" />, 'click',
    window.GMap_ServerClick);
  GEvent.addListener(<xsl:value-of select="$jsId" />, 'movestart',
    window.GMap_ServerMoveStart);
  GEvent.addListener(<xsl:value-of select="$jsId" />, 'moveend',
    window.GMap_ServerMoveEnd);
  GEvent.addListener(<xsl:value-of select="$jsId" />, 'zoom',
    window.GMap_ServerZoom);
</xsl:if>

A number of default events are "hooked up" to the Google Map. Rather than have the developer specify which JavaScript functions fulfill which events, default events named GMap_<Event Name> are attached to the Google Map. It is up to the developer to create these JavaScript functions if she wants to respond to the events. These events are all optional. This is accomplished by a little JavaScript hackery. If an event named GMap_Click is not found on the page or in a linked JavaScript file, an empty function _ef(), is used in its place. So, if you code it, it will run.

XML
GEvent.addListener(<xsl:value-of select="$jsId" />,
                              'click', window.GMap_Click||_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />,
                                'move', window.GMap_Move||_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />, 'movestart',
                                       window.GMap_MoveStart||_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />, 'moveend',
                                         window.GMap_MoveEnd||_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />,
                                    'zoom', window.GMap_Zoom||_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />, 'maptypechanged',
                                        window.GMap_MapTypeChanged||_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />, 'windowopen',
                                            window.GMap_WindowOpen||_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />, 'windowclose',
                                           window.GMap_WindowClose||_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />, 'addoverlay',
                                            window.GMap_AddOverlay||_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />, 'removeoverlay',
                                         window.GMap_RemoveOverlay||_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />, 'clearoverlays',
                                         window.GMap_ClearOverlays||_ef);

Two additional event binding allow the GMap to capture the map's state as it is moved or zoomed. Data is copied from the Google Map to the four HtmlInputHidden fields generated by the GMap.

XML
GEvent.addListener(<xsl:value-of select="$jsId" />,
                    'moveend', window.GMap_SaveState|_ef);
GEvent.addListener(<xsl:value-of select="$jsId" />,
                      'zoom', window.GMap_SaveState||_ef);
window.GMap_SaveState(<xsl:value-of select="$jsId" />);

The remainder of GMap.xsl is not as scary as it looks. Each map object is created with a corresponding XSL template. Each template creates the JavaScript necessary to create the Google Maps API object for the map. Default and client side callback events are also added to markers using the same naming conventions for the map as shown below:

XML
<xsl:if test="$enableClientCallBacks=true()">
  GEvent.addListener(<xsl:value-of select="$gmId" />,
                    'click', window.GMarker_ServerClick);
</xsl:if>
GEvent.addListener(<xsl:value-of select="$gmId" />,
                      'click', window.GMarker_Click||_ef);
GEvent.addListener(<xsl:value-of select="$gmId" />,
         'infowindowopen', window.GMarker_InfoWindowOpen||_ef);
GEvent.addListener(<xsl:value-of select="$gmId" />,
       'infowindowclose', window.GMarker_InfoWindowClose||_ef);

The completely transformed XML from the sample above is shown below:

JavaScript
<script type="text/javascript">

  var gMap_Js = null;
  function gMap_Render() {
    if( GBrowserIsCompatible()) {
      gMap_Js = new GMap( document.getElementById("gMap_Div"));
      gMap_Js.id = 'gMap';
      gMap_Js.zoomTo(1);
      gMap_Js.setMapType(G_MAP_TYPE); 
      gMap_Js.centerAndZoom(new GPoint(-105.5, 39), 10);
 
      GEvent.addListener(gMap_Js, 'click', 
                               window.GMap_ServerClick);
      GEvent.addListener(gMap_Js, 'movestart', 
                           window.GMap_ServerMoveStart);
      GEvent.addListener(gMap_Js, 'moveend', 
                             window.GMap_ServerMoveEnd);
      GEvent.addListener(gMap_Js, 'zoom', 
                                window.GMap_ServerZoom);
 
      GEvent.addListener(gMap_Js, 'click', 
                                window.GMap_Click||_ef);
      GEvent.addListener(gMap_Js, 'move', 
                                 window.GMap_Move||_ef);
      GEvent.addListener(gMap_Js, 'movestart', 
                            window.GMap_MoveStart||_ef);
      GEvent.addListener(gMap_Js, 'moveend', 
                              window.GMap_MoveEnd||_ef);
      GEvent.addListener(gMap_Js, 'zoom', 
                                 window.GMap_Zoom||_ef);        
      GEvent.addListener(gMap_Js, 'maptypechanged', 
                       window.GMap_MapTypeChanged||_ef);
      GEvent.addListener(gMap_Js, 'windowopen', 
                           window.GMap_WindowOpen||_ef);
      GEvent.addListener(gMap_Js, 'windowclose', 
                          window.GMap_WindowClose||_ef);
      GEvent.addListener(gMap_Js, 'addoverlay', 
                           window.GMap_AddOverlay||_ef);
      GEvent.addListener(gMap_Js, 'removeoverlay', 
                        window.GMap_RemoveOverlay||_ef);
      GEvent.addListener(gMap_Js, 'clearoverlays', 
                        window.GMap_ClearOverlays||_ef);
 
      GEvent.addListener(gMap_Js, 'moveend', 
                             window.GMap_SaveState|_ef);
      GEvent.addListener(gMap_Js, 'zoom', 
                            window.GMap_SaveState||_ef);
      window.GMap_SaveState(gMap_Js);
 
      var gMarker1 = new GMarker(
              new GPoint(-122.029,37.3953), BlueMarker);
      gMarker1.id='Motel 6 (#1054)';
      GEvent.addListener(gMarker1, 'click', 
                            window.GMarker_ServerClick);
      GEvent.addListener(gMarker1, 'click', 
                             window.GMarker_Click||_ef);
      GEvent.addListener(gMarker1, 'infowindowopen', 
                    window.GMarker_InfoWindowOpen||_ef);
      GEvent.addListener(gMarker1, 'infowindowclose', 
                   window.GMarker_InfoWindowClose||_ef);
      gMap_Js.addOverlay(gMarker1);
 
      gMap_Js.addControl(new GSmallMapControl());    
    }
  }
  window.addListener(window, 'load', gMap_Render);
  
  var BlueMarker = new GIcon(_defaultMarker.icon);
  BlueMarker.image = 
    "http://localhost/TestWeb/Advanced/blueMarker.png";
</script>

The JavaScript

The client callbacks and the postback data features of the GMap are encapsulated in the GMapX.js file found in the downloadable code. Most of the callback plumbing was built using my CallBackObject examined in another set of articles. Most of the other methods will fill in the blanks left up to this portion of the article.

The first few lines create some global variables used whenever a GMap is added to the page. There is also some code to add a method to the Google Maps API GMap object. getOverlayById() allows the developer to retrieve an overlay by a previously assigned string identifier. The next two lines add cross-browser support for attaching an event handler to a DOM object.

JavaScript
var _ef = function(){};
var _defaultMarker = new GMarker();
var _gMapRegX = new RegExp(":", "gi");
_gMapRegX.compile(":", "gi");
 
GMap.prototype.getOverlayById=function(a)
{
  for(var b=0;b<this.overlays.length;b++)
  {
    if(this.overlays[b].id==a)return this.overlays[b];
  }
  return null;
};

function addListener(a,b,c,d)
{
  if(a.addEventListener)
  {
    a.addEventListener(b,c,d);
    return true;
  }
  else if(a.attachEvent)
  {
    var e=a.attachEvent("on"+b,c);
    return e;
  }
  else
  {
    alert("Handler could not be attached");
  }
}

function bind(a,b,c,d)
{
  return window.addListener(a,b,
    function()
    {
      d.apply(c,arguments)
    }
  );
}

Next we'll skip to the GMap_Server<Event Name> functions. Each function builds an argument string including the server-side event name that should be raised and the event arguments that go with it in {event}|{argument1},{argument2}...{argumentN} form.

JavaScript
function GMap_ServerClick(overlay, point)
{
  var arg = 'GMap_Click|'+point.x+','+point.y;
  __DoCallBack(this, arg);
}

Remember how the events were bound to the GMap in the XML stylesheet? When these events are fired this represents the GMap causing the event (except for GMap_MarkerClick where this represents the marker being clicked). Using this, we can gather the required parameters to be sent back to the server to execute the server side event.

After the event argument is built, __DoCallBack() is called passing this (the GMap) and the arguments. __DoCallBack() constructs a new CallBackObject, assigns the OnComplete() and OnError() delegates, copies the state of the GMap with a call to GMap_SaveState(), and then performs the asynchronous request back to the server with cbo.DoCallBack().

JavaScript
function cbo_Complete(responseText, responseXML)
{
  eval(responseText);
}

function cbo_Error(status, statusText, responseText)
{
  alert('Error: ' + status + '\n' + statusText + '\n' + 
                                            responseText);
}

function __DoCallBack(eventTarget, eventArgument)
{
  var cbo = new CallBackObject();
  cbo.OnComplete = 
     function(){cbo_Complete.apply(eventTarget, arguments)};
  cbo.OnError = cbo_Error;
  window.GMap_SaveState(eventTarget);
  cbo.DoCallBack(eventTarget.id, eventArgument);
}

The interesting thing to note here is the assignment of the OnComplete() delegate.

  • The assignment is made using an anonymous function.
  • The function calls the apply() method on the cbo_Complete function passing eventTarget (which is the GMap or GMarker being acted upon)

Why do this rather than simply assign the cbo_Complete function? When the server returns its response from the callback, it will execute the cbo_Complete function. The cbo_Complete function simply interprets and executes the return value from the server. The return value should be JavaScript that the developer wants executed client side. The developer can now use this in her returned code to act upon the GMap or GMarker that raised the event. The State Quarters example from Part 2 makes use of this feature by calling the OpenInfoWindow() method of the GMarker representing a state.

Finally, GMap_SaveState() gets a reference to the GMap in question either through this or by the passed eventTarget. The code then gets reference to the HtmlInputHidden elements rendered by the GMap and updates them with the current map state values. Remember, this method is called before a client callback and just before the main form is submitted to the server as the result of a submit button click.

JavaScript
function GMap_SaveState(eventTarget)
{
  var evt = eventTarget.pan?eventTarget:this;
  var evtId = evt.id.replace_gMapRegX,'_');
  document.getElementById(evtId + '_CenterLatLng').value = 
                                      evt.getCenterLatLng();
  document.getElementById(evtId + '_SpanLatLng').value   = 
                                        evt.getSpanLatLng();
  document.getElementById(evtId + '_BoundsLatLng').value = 
                                      evt.getBoundsLatLng();
  document.getElementById(evtId + '_ZoomLevel').value = 
                                         evt.getZoomLevel();
}

Conclusion

Did that seem overly complicated to anyone else? Now that you've read through the article aren't you glad I encapsulated all that in a neat little server control? I'd love feedback from anyone who suffered through reading the entire article. Please let me know via the forum below which areas you felt were covered adequately and which features you feel need a little more explanation.

I recently got a copy of VS 2005 Beta 2 and have been spending a lot of time with Microsoft Atlas (hence the delay in publishing this article). Stay tuned for something in the near future on Atlas, Virtual Earth, and Markup Maps.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here