|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis 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:
The server controlI 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 First things firstThe first step in creating the 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: 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 methodsEach of the 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, CreateChildControls()In the 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 OnDataBinding()The DataSet ds = GetCounties();
gMap.DataSource = ds;
gMap.DataMarkerIdField = "CountyName";
gMap.DataLongitudeField = "Longitude";
gMap.DataLatitudeField = "Latitude";
gMap.DataBind();
Render()
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 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. private void Page_Load(object sender, System.EventArgs e)
{
//. . .
gMap.MarkerClick +=
new GMapClickEventHandler(gMap_MarkerClick);
//. . .
}
or <wcp:GMap runat="server" id="gMap" Width="750px" Height="525px"
EnableClientCallBacks="True" OnMarkerClick="gMap_MarkerClick"/>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. 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 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 XSLBefore I launch in to an explanation of GMap.xsl, I want to provide a few soap box remarks. Before working on the
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 <?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 <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 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 <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 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 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: <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: <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 JavaScriptThe client callbacks and the postback data features of the The first few lines create some global variables used whenever a 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.id==a)return this.overlays;
}
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 function GMap_ServerClick(overlay, point)
{
var arg = 'GMap_Click|'+point.x+','+point.y;
__DoCallBack(this, arg);
}
Remember how the events were bound to the After the event argument is built, 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
Why do this rather than simply assign the Finally, 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();
}
ConclusionDid 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.
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||