Virtual Earth meets MVC

Something I do quite a bit with customers is to help them understand and use Virtual Earth... plus I’ve been doing some overviews of the beta of the ASP.NET MVC Framework, so it seemed a great combination. How would you best combine them?

Note : This post is based on the October 2008 Beta of the ASP.NET MVC Framework... it will likely break before v1.0.

ASP.NET WebForms Approach

When hosting Virtual Earth in a WebForms solution, I use the following rough architecture;

The WCF service has two jobs; it provides lists of points to display on the map, and it provides additional information to render about each point if the user clicks a “more information” link. This info is usually shredded manually into the HTML by custom script.

There are a few problems with this;

1. You cannot currently use the <ASP:ScriptManager> control in ASP.NET MVC.

2. I want to minimize the amount of custom AJAX I write – the MVC framework has helpers that I want to use.

3. I suspect some of the partial rendering capabilities of the MVC framework will be useful.

An ASP.NET MVC Approach

My MVC approach is slightly different. On face value it looks complex, but in reality the implementation is very straightforward;

Well let’s get going. Hosting Virtual Earth is easy, so we’ll get the basics in place and see what we’ve got. Firstly I need to import the Virtual Earth controls with a simple script tag;

<script type="text/javascript" src="https://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6"></script>

Note I’m using version 6 – if a new version is out when you read this, you may need to update this link and revisit some of the code.

Now I’ll create a “div” to hold the map in my Master Page (do excuse some of the terrible CSS and layout in this post, I’ve just not got time to beautify my code today!);

<div id="LocationsMap"

style="position: relative; width: 100%; height: 100%;">

</div>

Next, I edit the body tag to cause the map to load with the page;

<body onload="DisplayMap()">

... and add a simple JavaScript function called DisplayMap that does the work. The core of this is creating the VEMap control, specifying the name of my “div”, as follows – for the rest, check out the code download;

map = new VEMap('LocationsMap');

This is all on my master page, and we’ve now got enough code to display a map the user can navigate around. To let us do this, simply add an Action to a Controller that returns a View(), which in turn is using the master page we’ve just defined;

public ActionResult Display()

{

return View();

}

Problem 1: No ScriptManager

The next thing I need to do is call my WCF service to fetch a list of “waypoints” to display on my map. But ASP.NET MVC isn’t compatible with the ScriptManager control. There are a few options here – the first is a codeplex project named ajaxmvc. What I’ve found to be a simpler solution, though, is to manually create the script reference that the ScriptManager creates for you;

<script src="<%= Url.Content("~/Services/WaypointService.svc/js") %>" type="text/javascript"></script>

I use the MVC framework’s “Url.Content” helper method to map the URL to my service, and append “/js” to ask the service for JavaScript proxy script. This now means I can call my service using the generated proxy;

function GetWayPoints() {

MvcWithVE.Services.IWaypointService.GetWaypoints(

SuccessCallback, FailureCallback);

}

The callback does the job of adding each waypoint to the map. Check out the code download to see how, but there is an important bit coming up...

Problem 2: Loading More Information

What I want to do next is load in more information about a selected waypoint, and display it in a panel beside the map. This just sounds ideal for the MVC framework’s partial rendering capabilities. If I can return a fragment of HTML and push it into the page, I’ll have saved loads of time.

Well, the Ajax.ActionLink helper method is useful here. I can provide it with a Controller and Action to call, and the ID of an HTML element to put the returned fragment into.

So let’s create a <span> to hold our fragment containing further information;

<span id="details">Please select a Waypoint...</span>

How do I get Virtual Earth to give me a “more information” link that populates this span then? The solution is two-fold. Firstly, I like to create a template for what will be displayed when I hover over a pin on the map;

<span id="pinTemplate" style="visibility: hidden;">

<div>

<p>

<b>{{Title}}</b>

</p>

<p>

This Waypoint is located at {{Latitude}} latitude,

{{Longitude}} longitude. For more information

<%= Ajax.ActionLink("click here...", "WaypointDetails",

new { id = "{{Id}}" },

new AjaxOptions { UpdateTargetId="details" })%>

</p>

</div>

</span>

This is a very simple bit of HTML, with markers in place (e.g. “{{Title}}”) that I can use to replace with real data. I guess I could put it in a separate User Control and use RenderPartial to pull it in if I wanted to. The cunning bit is that I also embed an Ajax.ActionLink call, providing a placeholder named {{Id}} for the “id” parameter to the WaypointDetails action. I’ve also specified the UpdateTargetId parameter to AjaxOptions as “details”, the name of the span I created above.

Now consider the code I use to add a pin to the map;

// Adds a waypoint to the map

function AddPoint(id, name, latitude, longitude)

{

// create the pin

var pin = new VEShape(VEShapeType.Pushpin,

new VELatLong(latitude, longitude));

// load in the template for the pin

var content = document.getElementById('pinTemplate').innerHTML;

// replace fields with values

content = content.replace('{{Title}}', name);

content = content.replace('{{Latitude}}', latitude);

content = content.replace('{{Longitude}}', longitude);

content = content.replace(escape('{{Id}}'), id);

// set the pin's content to the parsed template

pin.SetDescription(content);

// enable the pin's icon

pin.ShowIcon();

// add it to the map

map.AddShape(pin);

}

I load in my template and replace the fields with real data. The one thing that might trip you up is that I must URL-encode the “{{Id}}” placeholder using the JavaScript “escape” function in order for the replace method to match it, as the Ajax.ActionLink will have URL-encoded it already. You can see this by looking at the HTML source for a rendered web page, and note that the link has become;

... href="https://blogs.msdn.com/Map/WaypointDetails/%7B%7BId%7D%7D" ...

We then use the pin.SetDescription call to ask Virtual Earth to display this whole fragment when a pin is hovered over.

The very last piece of the puzzle is that we need an Action named “WaypointDetails” that the Ajax.ActionLink method is pointing at. This is really simple in my case, as I’m just using LINQ-to-SQL to pull back some data;

public ActionResult WaypointDetails(int id)

{

using (WaypointDataContext db = new WaypointDataContext())

{

Waypoint point = db.Waypoints.Single(w => w.Id == id);

return View(point);

}

}

Of course, I have a matching View named “WaypointDetails.ascx”, which looks like this;

<table class="WaypointInfoTable">

<tr>

<th>Title</th>

<td><%= ViewData.Model.Title %></td>

</tr>

<tr>

<th>Latitude</th>

<td><%= ViewData.Model.Latitude.ToString() %></td>

</tr>

<!-- snip -->

</table>

To round off the solution, I’ve added a link to call GetWaypoints() when the user clicks on it, tweaked the routing to default to my new Controller, and deleted some of the stuff the MVC project template gives me by default that I don’t need.

Phew!

OK, I’ve been through this really quickly because I think the code is pretty self-explanatory, but I hope you’ve enjoyed seeing how easy this is. What is more, it gives you a great way to separate responsibilities for rendering the host page, rendering fragments, hosting your logic, fetching data, and more.

The end result is pretty cool, I think! Like it?

 

MvcWithVE.zip