Custom Editor Template for Geo Coordinates in MVC 3 using Google Maps

... by

I was recently given the task to make stored geo location data editable. So ideally I would want to have an Edit view where I can just move a pin around on a map. Something similar has been done in the nerddinner demo application. The developers have of course used Microsoft’s Bing maps. Nothing wrong with that, but I personally prefer Google Maps.

At the end of this tutorial, you will have an Edit / Create View that looks like this:

Location Editor

You can move the pin on the map which will update or create the location of the model, once you press the Create button.

Let’s get started.

1. The Model

We want to create a very basic model called Place with a name and a pair of map coordinates.

public class Place
{
    public Place()
    {
        Pos = new GeoCoords() { Latitude = 0, Longitude = 0 };
    }

    public string Name { get; set; }
    public GeoCoords Pos { get; set; }
}

public class GeoCoords
{
    public double Latitude { get; set; }
    public double Longitude { get; set; }
}

It is important, that the GeoCoords are in a separate class. I’ll explain shortly.

2. The View

To create the View, use the standard MVC scaffolding. Unfortunately, the Pos attribute is just omitted because the scaffolder doesn’t know what to do with custom classes. So you need to add the standard edit code for the Pos attribute:

<div class="editor-label">
    @Html.LabelFor(model => model.Pos)
</div>
<div class="editor-field">
    @Html.EditorFor(model => model.Pos)
    @Html.ValidationMessageFor(model => model.Pos)
</div>

This is the same code, MVC would generate for a simple type attribute. This makes for very clean code within the View, as you wouldn’t be able to tell that the Pos attribute needs any special treatment. You’ve probably guessed by now, that we will use a Custom Editor Template. This tells MVC how to render an editor for an attribute of type GeoCoords.

For this, you need to create a new partial View called GeoCoords.cshtml in Views\Shared\EditorTemplates. Create the EditorTemplates folder if it doesn’t exist.

Location Editor Solution Tree

Then edit the code in GeoCoords to look like this:

@model GeoCoordsTest.Models.GeoCoords

@{
    string name = ViewData.TemplateInfo.HtmlFieldPrefix;
    string id = name.Replace(".", "_");
    string lat = id + "_Latitude";
    string lon = id + "_Longitude";
}

@Html.HiddenFor(model => model.Latitude)
@Html.HiddenFor(model => model.Longitude)

<script type="text/javascript" src="https://www.google.com/jsapi?autoload={'modules':[{name:'maps',version:3,other_params:'sensor=false'}]}"></script>

<script type="text/javascript">
    var marker;
    
    function init() {
        var mapDiv = document.getElementById('canvas');
        var map = new google.maps.Map(mapDiv, {
            center: new google.maps.LatLng(
                @Model.Latitude, @Model.Longitude),
            zoom: 8, 
            mapTypeId: google.maps.MapTypeId.ROADMAP
        });
        var pos = new google.maps.LatLng(
            @Model.Latitude, @Model.Longitude);
                
        marker = new google.maps.Marker({
            map: map,
            position: pos,
            title: "",
            draggable: true
        });
                
        google.maps.event.addListener(marker, 'drag', function() {
            var pos = marker.getPosition();
            $("#@lat").val(pos.lat());
            $("#@lon").val(pos.lng());
        });
    }

    google.maps.event.addDomListener(window, 'load', init);    
</script>

<div id="canvas" style="height:300px;width:300px;"></div>

Let’s step through the code. First, two hidden fields are created.

@Html.HiddenFor(model => model.Latitude)
@Html.HiddenFor(model => model.Longitude)

These fields will hold the values of Latitude and Longitude. We will use JavaScript to update the values as the user drags the map pin around. In order to manipulate these hidden fields, we will need to know their ID tag. This part

@{
    string name = ViewData.TemplateInfo.HtmlFieldPrefix;
    string id = name.Replace(".", "_");
    string lat = id + "_Latitude";
    string lon = id + "_Longitude";
}

creates to server-side variables, lat and lon which hold the ID tags of our hidden fields. MVC follows the convention of using the name of the calling model attribute, in our case “Pos” combined with the internal attribute names using an underscore. In our case, this yields “Pos_Latitude” and “Pos_Longitude”. The name of the calling model attribute can be obtained by using ViewData.TemplateInfo.HtmlFieldPrefix. After the Google Maps script reference, the map is created.

<script type="text/javascript">
    var marker;
    
    function init() {
        var mapDiv = document.getElementById('canvas');
        var map = new google.maps.Map(mapDiv, {
            center: new google.maps.LatLng(
                @Model.Latitude, @Model.Longitude),
            zoom: 8, 
            mapTypeId: google.maps.MapTypeId.ROADMAP
        });
        var pos = new google.maps.LatLng(
            @Model.Latitude, @Model.Longitude);
                
        marker = new google.maps.Marker({
            map: map,
            position: pos,
            title: "",
            draggable: true
        });

The map is centered on the given map coordinates. Also, a draggable marker is created. The interesting bit is where the draggable marker is connected to the hidden fields:

google.maps.event.addListener(marker, 'drag', function() {
    var pos = marker.getPosition();
    $("#@lat").val(pos.lat());
    $("#@lon").val(pos.lng());
});

Whenever the marker fires the “drag” event, the hidden fields are identified using jQuery an the server-side variables we created earlier. Then their value is set to the current marker position. This means, that when the user clicks the submit button in the original form, the hidden fields will hold the last value of the draggable marker and will be posted back to the server.

3. The Controller

There is nothing special here. Using the Custom Template will update the model fields allowing the Controller to save the Latitude and Longitude values to the database.

public ActionResult CreatePlace()
{
    return View(new Place());
}

[HttpPost]
public ActionResult CreatePlace(Place model)
{
    if (ModelState.IsValid)
    {
        // save the place
    }
    return View(model);
}

4. Result

You should now have a reusable editor template that displays an embedded Google Map whenever a GeoCoords object is edited in a View.

Thomas Glaser

@tkglaser
...

About me

Web & Mobile Engineer, Founder, Lean Startup Enthusiast.

tk glaser consulting

Social Links