Author:
Darren Neimke
Live Maps is a map interface that allows us to do many things when working with locational data. The user experience for working with a specific location on the map is exposed through pins that are placed on the map and used to associate additional data about that location. This data typically includes things such as its name, an image, and a description about it. Having saved a location on the map you can then view this information by simply placing your mouse cursor over the pin, at which point, a pop-up panel is displayed with the information displayed on it as seen in the following image.
This pop-up panel is referred to as the 'InfoBox'. As you can see, the InfoBox not only displays the title and description for the location but also contains a set of links at the bottom of the panel - also known as verbs - that the user can use to perform actions upon the location. These action verbs include things such as allowing the user to edit the information about the location or to send its details by email to a friend.
When creating custom map-based applications based on Virtual Earth, we can use the Virtual Earth API's to add pushpin's to the map similar to those seen in the Live Maps application. The following snippet of Javascript code shows how to add a pushpin to the Virtual Earth map:
var shape = new VEShape(VEShapeType.Pushpin, loc);
shape.SetTitle('My pushpin');
shape.SetDescription("This is the description");
map.AddShape(shape);
Hovering over our custom pin displays the title and description that were added in our code as we would expect:
Typically, we will also need to display verbs against our own custom pins in a similar manner to those that are available on pins to provide users of our applications access to similar features to those seen in the Live Maps application. The simple way to achieve this result is to generate some custom HTML with links embedded within it and then set it as the description for the pin like so:
var html = "<p>This is the description.</p>" +
"<div style='background-color: #e4edf3; padding: 5px'>" +
"<a href='#' onclick='alert(\"foo\");'>Say Foo</a>" +
"</div>";
shape.SetDescription(html);
This time, when we hover over the pin, not only does the InfoBox display with the title and description as before, but we now also have a custom verb that is displayed too. Click on the verb and see that the javascript function is invoked correctly:

It is great news that it's so easy to add features which are similar to those found in the Live Maps application. All we've had to do is to write a dozen or so lines of javascript and we have an InfoBox which looks very similar to the Live Maps one and which will presumably allow our users to perform similar actions as well.
However, while the results that we've achieved with so little code are impressive, there are some shortcomings to our approach. By simply stuffing text into a property, we are losing a lot of the fine-grained control that we could get from dealing with individual DOM elements. Also, in mixing our data, with behavioral code, and presentation structure we also end up creating quite a mess for ourselves as the scenarios become more sophisticated.
Creating an InfoBox API
In our previous simple verb example, there was very little data or behavioral logic to worry about. But consider a more real world scenario where you have many potential behavioral issues to worry about, such as where verbs are only visibile based on certain runtime conditions. Or data concerns such as where the handler function for the verb requires a handle to the instance of the map, or to an instance of the actual pin. Using raw strings for either of these examples will lead us down the path of messy string building in amongst our behavioral code and uneccessary state variables scattered throughout our code. To alleviate these problems we are going to create a reusable InfoBox Javascript class that will encapsulate the logic for accessing the InfoBox at runtime to perform tasks such as adding verbs. When we have finished, our code for adding a Verb to the InfoBox will look like this:
var editVerb = new Verb("Edit", EditDelegate);
map.InfoBox.AddVerb(editVerb) ;
Having classes for our verbs will allow us to encapsulate the data and logic properly within the class as well as maintaining the verb behavior in a single place. This encapsulation allows us to create much smarter verbs without having to create reams of hard to understand string manipulation code. In our example today these verbs will have a simple title property and delegate to invoke when they are clicked. In another application you might also extend the Verb class to include context data - such as references to the map instance or other user defined data.
Similarly, having a class for the InfoBox itself will encapsulate the way that we add the verb elements to the InfoBox user interface as opposed to having that code sprawled out among the behavioral code for our menu - or worse still, the page.
The InfoBox class
Our custom InfoBox class will be implemented as an ASP.NET Ajax component, deriving from the Sys.UI.Control class. This means that any developers who use our class will be able to implement common ASP.NET Ajax coding patterns - such as using $create to create instances of our class as well as gaining the resource and memory management features provided by the Sys.IDisposable interface. The purpose of this class will be to provide an API that gives access to a list of Verbs in addition to holding a reference to the map that it is joined to so that it can communicate with it privately. This helps the class to be more self-contained and therefore more reusable against any instance of the VEMap class.
To get the ball rolling, we create a new Javascript file and add the following skeleton code for our InfoBox class:
InfoBox = function(map) {
this._verbElement = null ;
this._verbs = [] ;
this._map = null ;
} ;
InfoBox.prototype = {
dispose: function() {
$clearHandlers(this._verbElement);
this._verbElement = null;
this._verbs = null ;
InfoBox.callBaseMethod(this, 'dispose');
},
initialize: function() {
InfoBox.callBaseMethod(this, 'initialize');
}
} ;
InfoBox.registerClass('InfoBox', Sys.UI.Control);
if (typeof(Sys) !== "undefined") Sys.Application.notifyScriptLoaded();
Starting from the top of this code snippet we first create a Javascript constructor function called InfoBox. This function has variables that hold references to all of the data that we need. It has a reference to the DOM element that we will act as the container element for our verbs. It will also hold the instances of our verbs and also to the VEMap instance that instantiated it.
After the constructor function we fill the prototype object with dispose and initialize methods - that we will use to make sure that we clean up any memory that we have used when the InfoBox class is disposed. Finally, we make a call to the ASP.NET Ajax registerClass method to register our InfoBox class with the ASP.NET Ajax Library. Registering our class ensures that we get access to the previously mentioned benefits that the ASP.NET Ajax brings to the table when working with client side components.
The next thing to do is to add getter and setter properties to our class. In ASP.NET Ajax these are implemented by adding more Javascript functions to the prototype object using a special naming convention. The getter for a property is a method whose name is prefixed by 'get_' while the setter method is prefixed by 'set_'. Add getters and setters for each of the three fields in our class to the prototype object. Here's the property implementation for the _verbs field:
get_Verbs : function() {
return this._verbs;
},
set_Verbs : function(value) {
this._verbs = value;
},
Soon we'll get to see how ASP.NET Ajax deals with these specially named properties when we use the $create syntax to create an instance of our class. In the meantime we'll add another couple of methods to our InfoBox's prototype object to make it easier to add and remove verbs to the InfoBox instance. Add the following 2 methods to the body of the prototype object:
AddVerb: function(verb) {
this._verbs.push(verb) ;
},
ClearVerbs: function() {
this._verbs = [] ;
},
Now that we can add and remove Verbs to our InfoBox, all that remains is to add the method that is responsible for actually adding the verbs to the UI of the InfoBox and displaying it on the screen. This method will be called whenever the InfoBox is required to be displayed and its job is to cycle through whichever verbs have been added to its Verbs collection and render them within the body of the InfoBox pop-up. Add the following method to the body of the prototype object as well:
Show: function(shape) {
var e = this.get_VerbElement() ;
if( e.hasChildNodes() ) {
for( var i=e.children.length-1; i>=0; i-- ) {
$clearHandlers(e.children[i]) ;
e.removeChild(e.children[i]) ;
}
}
for( var i=0; i<this._verbs.length; i++ ) {
var verb = this._verbs[i] ;
var item1 = document.createElement("li") ;
var link1 = document.createElement("a") ;
link1.setAttribute('href','javascript:void(0);');
link1.appendChild(document.createTextNode(verb.Title)) ;
$addHandler(link1, "click", verb.Handler);
item1.appendChild(link1) ;
e.appendChild(item1) ;
}
this._map.ShowInfoBox(shape);
},
The Show method clears any verbs that we may have added previously to the verb element and cleans up the resources that they were using too. After clearing all elements from the verb element we then enumerate all Verbs that have been added to our collection and create HTML elements to display them and add them to the DOM. Finally, using the reference that we have to the VEMap instance, we display the InfoBox for the current shape.
The last thing we need to do is to create a simple data class which represents our verbs. To keep things simple, we'll just create a function with a title to display and a delegate that will be invoked whenever the verb is fired.
Verb = function(title, delegate) {
this.Title = title ;
this.Handler = delegate ;
} ;
We could have created this as an ASP.NET Ajax client class, as we did for the InfoBox, but for the purposes of simplicity we'll leave it as a plain old Javascript function for now. In your own applications you may however wish to extend the logic within the Verb class, in which case it may make perfect sense to promote it to a proper ASP.NET Ajax component.
Adding our InfoBox to the VEMap
Now that we have our InfoBox and Verb classes complete, it's time to work out how best to instantiate the InfoBox and reference it from within our VEMap instance. The first thing that we can do here is to extend the prototype object for the VEMap class to include an InfoBox property. We do this like so:
VEMap.prototype.InfoBox = null ;
To create an instance of our InfoBox class and set it to the new VEMap.InfoBox property we'll use the $create ASP.NET Ajax shortcut to ensure that our class is wired-up properly as a Sys.Component. The Sys.Component class requires that we associate our component with a particular HTML DOM element which, in our case, means working out what HTML Virtual Earth generates and then from within that, referencing the DOM element that it uses to represent the InfoBox elements. The following link points to a useful article which describes the format of the HTML that is produced for the Virtual Earth InfoBox:
Virtual Earth 5 Custom Info box
The HTML that is produced by Virtual Earth for the existing InfoBox therefore looks something like this:
<div id="MSVE_navAction_palette"> </div>
<div class="undefined" style="visibility: hidden">
<div class="ero-shadow">
<div class="ero-body">
<div class="ero-actionsBackground">
<div class="ero-previewArea"> </div>
<div class="ero-actions">
<ul></ul>
</div>
<div class="ero-paddingHack"> </div>
</div>
</div>
</div>
<div class="ero-beak">
</div>
</div>
<div class="ero-progressAnimation" style="visibility: hidden">
</div>
One thing that you may notice about this HTML is that none of the elements for the actual InfoBox elements appear to have ID's associated with them. So the question that begs is, how do we find the InfoBox HTML in the page. Luckily for us, Virtual Earth actually emits a placeholder DIV element which it places immediately before the HTML shown in that article and which it assigns the ID of 'MSVE_navAction_palette'. Therefore, in order to get a handle to the HTML of the InfoBox we must first find the 'MSVE_navAction_palette' element and then grab the next sibling element from the DOM tree. The following function is responsible for locating the InfoBox element and using it to instantiate our InfoBox component before finally assigning that component instance to the InfoBox property on our VEMap instance:
function CreateInfoBox() {
var InfoBoxElement = $get('MSVE_navAction_palette').nextSibling ;
var verbsListElement = null ;
var divs = InfoBoxElement.getElementsByTagName("div") ;
for( var i=0; i<divs.length; i++ ) {
var e = divs[i] ;
if( e.className == "ero-actions" ) {
verbsListElement = e.childNodes[0] ;
}
}
var InfoBox = $create(
InfoBox,
{
'VerbElement': verbsListElement,
'Map' : map
},
null,
null,
InfoBoxElement
);
map.InfoBox = InfoBox;
}
Notice that we start by getting a reference to the nextSibling of the placeholder element and then we loop through each of the DIV child elements within that container to find the one with a class name of 'ero-actions'. We then store this element reference in the VerbElement property of our custom InfoBox class as you can see from the $create statement near the bottom of the snippet. Finally, the component instance of our custom InfoBox is assigned to the InfoBox property of the map.
This means that we can now refer directly to the InfoBox from our map instance to control which verbs are present and to show the InfoBox.
Taking it for a spin!
All All that's left to do now is to write some code to take our new InfoBox class for a bit of a test spin. The best place to start is to create a new ASP.NET Ajax page and wire up the page's load event so that we can instantiate an instance of the map and add our custom InfoBox to it. Start with the following code which creates a map and attaches a handler to its mouseover event so that we can know when to display the InfoBox.
function pageLoad() {
map = new VEMap('theMap') ;
map.LoadMap(null, 7) ;
this._layer = new VEShapeLayer();
map.AddShapeLayer(this._layer);
AddShapes() ; // randomly add a few shapes to the map
mapMoveDelegate = Function.createDelegate(this, this._MapMoveHandler);
map.AttachEvent("onmouseover", mapMoveDelegate);
CreateInfoBox() ;
}
The The method starts with the standard code for instantiating the map and adds a shape layer to it - as good practice dictates. Next up is a method call which adds some random pins to our map so that we have some pins to use as mouseover targets for testing our InfoBox against. We then hook up our onmouseover handler and finally we create the actual InfoBox and bind it to the map.
There's nothing terribly special about the AddShapes method; as I said, it is just responsible for adding a couple of random locations to our map so that we have some pins to test against:
function AddShapes() {
var latLong = map.GetCenter();
var shape = new VEShape(VEShapeType.Pushpin, latLong);
shape.SetTitle('<h1>A Pin</h1>');
shape.SetDescription('<div>This pin is located at <b>' + latLong + '</b>.</div>');
this._layer.AddShape(shape);
latLong = new VELatLong(latLong.Latitude + 1, latLong.Longitude + 1) ;
shape = new VEShape(VEShapeType.Pushpin, latLong);
shape.SetTitle('<h1>Another Pin</h1>');
shape.SetDescription('<div>This pin is located at <b>' + latLong + '</b>.</div>');
this._layer.AddShape(shape);
}
The The onmouseover handler - _MapMoveHandler - is where we handle the mouseover event. We do this to determine whether or not we are over a pin. If we are over a pin then we can work out which verbs to display and show the InfoBox for that shape.
function _MapMoveHandler(e) {
if (e.elementID) {
var shape = map.GetShapeByID(e.elementID)
if (shape) {
AddVerbs(shape) ;
map.InfoBox.Show(shape);
e.preventDefault();
}
}
}
In tIn the case of this example, we'll be simply adding the same verbs each time but the beauty of our implementation is that we could just as easily be dynamically populating the verbs based on all sorts of logic. Some typical logic might be to determine whether or not a particular location exists in one of the users favorite collections and, if not, to display a verb that allows them to 'Add to Favorites'. Other examples of conditional logic here could be that you might only display a 'Zoom to Region' verb for a given place if the current zoom level is greater than a certain factor. Here's the code for adding 2 simple verbs to the InfoBox:
function AddVerbs(shape) {
map.InfoBox.ClearVerbs() ;
var EditDelegate = Function.createDelegate(this, this._EditHandler);
var SaveDelegate = Function.createDelegate(this, this._SaveHandler);
var editVerb = new Verb("Edit", EditDelegate);
var saveVerb = new Verb("Save", SaveDelegate);
map.InfoBox.AddVerb(editVerb) ;
map.InfoBox.AddVerb(saveVerb) ;
}
In looking at the AddVerbs function, we can see the flexibility that we get from having an object model for our verbs as opposed to simply passing a big fat string to a property. Here we see that we are creating delegates and passing them into the constructor for our Verb object. Having delegates allows us to easily maintain a reference to menu instance without having to rely on specifically named data variables floating around in our page's Javascript file. This makes our code more encapsulated and also much more reusable.
Sample Source Code Link Below
You must download and install MS ASP.NET AJAX to run this code.
Conclusion
So there you have it, a nice, reusable object model which wraps the Virtual Earth InfoBox. The InfoBox and Verb classes that we've coded up are all ready for you to use in your next Virtual Earth project and to extend with your own additional logic and functionality.
Happy Coding!