Hey I'm Lee. My blog was put up to house my strange thoughts, ramblings, nuggets of information I can refer back to and document my learning curves on new dev stuff and fitness regimes.

All thoughts and comments on here are my own, and in no way reflect my employer - I also take no responsibility for spelling, grammar, terminology, accuracy of facts etc... So read at your own risk!

Creating A Google Map Property Editor For Umbraco v5

If you want to follow this little tutorial then download the current V5 source by following my previous post, and get yourself set up and the source running.  This post will focus more on the Umbraco fundamentals/code than the underlying JS code that makes the Google Map Geocode and Marker Move etc.. If you want more info on that then see the Google Map Post.

As with most blog posts, I will explain things in my own way so the terminology may not be 100% correct, but I hope at the end of this post you should be able to easily create your property editor.  also before I start if you are coming from Umbraco v4 and want to know what the hell a property editor is… Well its sort of the new name for DataTypes.

References

Before I start there are some good references, albeit things are still changing so they may need to be updated

Wiki post
http://jupiter.umbraco.org/Developing%20Property%20Editors.ashx

Tim's Post
http://www.nibble.be/?p=104

MVC validation data annotations
Lastly I would highly recommend you read up about MVC validation data annotations, as you use these for the EditorModel and prevalueModel model

http://www.asp.net/mvc/tutorials/validation-with-the-data-annotation-validators-cs

Simple Example

When I started messing around, I found the simplest example of a property editor was to open up the v5 source and go to the 'Umbraco.Cms.Web.PropertyEditors' project and open up the TrueFalse property editor.  As you can see this is a very, very simple property editor and is made of two files 'TrueFalseEditor' and 'TrueFalseEditorModel'.  Notice the naming convention of PropertyNameEditor and PropertyNameEditorModel, this is kept throughout.

true-false1

true-false2

The only file that's really doing anything here is the 'TrueFalseEditorModel', if you open it up you can see its a simple class with a public bool property (Decorated with data annotations - See link in references above) and it inherits from 'EditorModel' - That's it, which is why its called the 'Model' (I look at this like scaffolding in MVC, I guess its probably the something similar under the hood?). 

It literally is the model of your property editor, in this case with are just trying to display something which enables a user to choose and save a true/false value.  So when this bool property in your model is picked up by Umbraco, it will render it as a checkbox in the view. The second file 'TrueFalseEditor' in this case is just used to return your EditorModel to Umbraco and also has some identifiers in the decoration, it has a unique GUID which is the ID, an alias and a name - But in short all you need to know for this example is that it returns your EditorModel which is rendered as a checkbox.

So if we changed the property named 'Value' in the Model from a bool to say a string, then in the admin it would get rendered as textbox… Same if we changed it from a bool to say an int, it would get rendered as a textbox that only accepts an integer! Yes validation is all handled for you! Pretty cool hey…

TIPS:

1) I'm not sure on the terminology here, but Property editors are fully compiled meaning that if you have any assets (Custom Views, JS & CSS files etc…) they need to be marked as embedded resource so they are compiled into the DLL. We'll cover this again but to do this you just 'right click' on the files and go to 'properties' and change build action to embedded resource (see below)

embedded

2) Whether this will be sorted on full release or not I don't know, because I don't know if its an MVC or Umbraco thing. But if you make some changes to your property editor, or add some more resource files it doesn't always show up when Umbraco is run.  To kick the changes into effect you need to delete you ASP.NET temp files and then rebuild the solution (Bit of a pain but not a show stopper).

Structure

As you will see from other Property Editors in the v5 source the file/folder structure is pretty simple - Two folders:

Views
Resources

Which hold what the say on the tin… any custom views go into the Views folder and your resources (CSS, JS, Images) go into the Resources folder.  all you classes then go in the root of the editor folder, using the naming convention I mentioned above. Again don't forget any views/resources added have to marked as 'embedded resource' or your property editor won't work.

The gMap Model

I have to say, (and I hope you'll agree after reading the above) making a quick, simple'ish custom datatype in v5 really is a breeze - I love the way you can just create your model class and don't have to worry about creating the UI or worry about how to save the values… It just works.  But for our Google Map property editor we need to add some custom code to the view, so we can show the map, autocomplete textbox for locations and a few other things.

But again, the v5 guys have made doing this SUPER easy!  Before we do that we still need to create our base model and editor class.  So just like the other property editors, open up the v5 source and in the 'Umbraco.Cms.Web.PropertyEditors' project create a folder called 'GoogleMap' and create the following files

Views (Folder)
Resources (Folder)
GoogleMapEditor.cs
GoogleMapEditorModel.cs

startstructure

Now we just need to add the properties to our Model - Lets work our what we need, and then we can relate that to the model - All I want to store is:

  • The Location The Users Types In (string)
  • The longitude from the gMaps GeoCoding (User doesn't use these, so they can ben hidden)
  • The latitude from the gMaps GeoCoding (User doesn't use these, so they can ben hidden)

Based on this I'll create both the editor and editorModel just like its been done in the simple TrueFalse example above:

Open GoogleMapEditorModel.cs and add

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using Umbraco.Cms.Web.EmbeddedViewEngine;
using Umbraco.Cms.Web.Model.BackOffice.PropertyEditors;
using Umbraco.Cms.Web.PropertyEditors.Tags;

namespace Umbraco.Cms.Web.PropertyEditors.GoogleMap
{
    public class GoogleMapEditorModel : EditorModel
    {

        [Required(ErrorMessage = "Postcode/location is required")]
        public string Postcode { get; set; }

        [HiddenInput(DisplayValue = false)]
        public string Long { get; set; }

        [HiddenInput(DisplayValue = false)]
        public string Lat { get; set; }

    }
}

I'm not going to go into detail about the attributes I have decorated the properties with, as I mentioned above you need to go and read up about 'MVC validation data annotations' as they are an MVC thing not a specific Umbraco thing.  But I hope just be looking at the code you get the idea of what they are doing.

Open GoogleMapEditor.cs and add

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;
using Umbraco.Cms.Web.Model.BackOffice.PropertyEditors;

namespace Umbraco.Cms.Web.PropertyEditors.GoogleMap
{
    [PropertyEditor("E8FA2010-D72E-4F13-9C88-9B79F8466934", "GoogleMap", "GoogleMap")]
    public class GoogleMapEditor : PropertyEditor<GoogleMapEditorModel>
    {
        public override GoogleMapEditorModel CreateEditorModel()
        {
            return new GoogleMapEditorModel();
        }

    }
}

As you can see a very simple bit of code… And you could now build this code, and it would work (So to speak) but you would see a textbox that required some text in the admin and it would have two hidden inputs which would do nothing! Awesome hey! What we need to do now is add a custom view to start making this work.

Adding A Custom View

To add a custom view for our property editor we just need to create the view in the 'Views' folder and mark the Editor Model as an 'EmbeddedView' with the path to the view.  So lets do that, create your View and add it into the views folder keeping the same naming convention as the actual editor.

GoogleMapEditor.cshtml

And copy and paste the following in and save it

@inherits WebViewPage<Umbraco.Cms.Web.PropertyEditors.GoogleMap.GoogleMapEditorModel>
@using Umbraco.Cms.Web;
@using ClientDependency.Core;
@using ClientDependency.Core.Mvc;
@using System.Web.Helpers;
@using System.Web.Mvc;
@using System.Web.Mvc.Ajax;
@using System.Web.Mvc.Html;
@using System.Web.Routing;
@using System.Web.WebPages;
@using Microsoft.Web.Mvc;
@using Umbraco.Cms.Web.PropertyEditors.GoogleMap

Now in your EditorModel class you just need to decorate the class with the following (NAMESPACES ARE CASE SENSITIVE, so make sure you double check them - This has caught me out before)

    [EmbeddedView("Umbraco.Cms.Web.PropertyEditors.GoogleMap.Views.GoogleMapEditor.cshtml", "Umbraco.Cms.Web.PropertyEditors")]
    public class GoogleMapEditorModel : EditorModel

Hopefully you can see the two main things we have done here, in the view we have added the '@inherits WebViewPage<>' and on the EditorModel added '[EmbeddedView()]' I think these are pretty self explanatory so won't go into anymore detail on these.  But in short, the your telling the view to use the EditorModel as the 'model' in the view so you have access to all the properties and also just making sure Umbraco knows that the EditorModel is now an EmbeddedView with the path to it.

Now things are going to move a little faster here because we are just going to add the HTML, JS & CSS… Again I mentioned at the beginning of the blog I wasn't going to go into much detail about the Google Map stuff as its all in my previous post.  So with that, open up your new custom view and replace it with the following and save it:

@inherits WebViewPage<Umbraco.Cms.Web.PropertyEditors.GoogleMap.GoogleMapEditorModel>
@using Umbraco.Cms.Web;
@using ClientDependency.Core;
@using ClientDependency.Core.Mvc;
@using System.Web.Helpers;
@using System.Web.Mvc;
@using System.Web.Mvc.Ajax;
@using System.Web.Mvc.Html;
@using System.Web.Routing;
@using System.Web.WebPages;
@using Microsoft.Web.Mvc;
@using Umbraco.Cms.Web.PropertyEditors.GoogleMap

<script type="text/javascript">
    var PostCodeid = "#@Html.IdFor(x => Model.Postcode)";
    var longval = "#@Html.IdFor(x => Model.Long)";
    var latval = "#@Html.IdFor(x => Model.Lat)";
</script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
@{ 
Html
.RequiresJsResource(typeof(GoogleMapEditor), "Umbraco.Cms.Web.PropertyEditors.GoogleMap.Resources.gMap.js")
.RequiresCssResource(typeof(GoogleMapEditor), "Umbraco.Cms.Web.PropertyEditors.GoogleMap.Resources.gMap.css");
}

<p>@Html.TextBoxFor(x => Model.Postcode, new { @class = "postcode" }) <input type="submit" id="findbutton" value="Find" /></p>

<p>@Html.TextBox("latTextBox", Model.Lat, new { @class = "latTextBox", @readonly =  "readonly" }) @Html.TextBox("longTextBox", Model.Long, new { @class = "longTextBox", @readonly = "readonly" })</p>

<div id="geomap" style="width: 350px; height: 350px;">
    <p>
        Loading Please Wait...</p>
</div>

@Html.HiddenFor(x => Model.Lat)
@Html.HiddenFor(x => Model.Long)

As this is a simple view, you should be able to see what everything is - I'll break down what's on the page starting from the top

  • We have some global JS variables which get the ID's of elements we use in our JS code
  • Reference the Google maps API
  • Reference the two asset files which we'll add in the next section (JS & CSS)
    - TIP: Whenever adding resources, note that the namespace IS case sensitive
  • HTML for the view (Showing the textbox, a find button, two general textboxes to show the Long & Lat, Div for the map and the two hidden fields to store the Long & Lat)

REMEMBER: Now we are done, mark this view as an embedded resource (Right click, properties, build action = embedded resource)

Adding Resources

In this property editor with have two asset/resource files - One JavaScript file with all the logic to interact with GoogleMaps and a very simple CSS file just so you can see the autocomplete dropdown and a bit of other styling.  Add the JS & CSS file in your 'Resources' folder like so  'gMap.css & gMap.js'

full-structure

Now add the contents of them

gMap.css

.ui-autocomplete { background-color: white; width: 300px; border: 1px solid #cfcfcf; list-style-type: none; padding-left: 0px; font-family: Arial, Helvetica, sans-serif; cursor: pointer; font-size: 12px; }
.ui-menu-item { padding: 4px 0; }
#geomap { border: 1px #ccc solid;}
input.latTextBox, input.longTextBox { width: 120px !Important;}

gMap.js

var geocoder;
var map;
var marker;

function initialize() {
    //MAP
    var initialLat = $(latval).val();
    var initialLong = $(longval).val();
    if (initialLat == '') {
        initialLat = "51.773071843208115";
        initialLong = "-1.6568558468750325";
    }
    var latlng = new google.maps.LatLng(initialLat, initialLong);
    var options = {
        zoom: 16,
        center: latlng,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    };

    map = new google.maps.Map(document.getElementById("geomap"), options);

    geocoder = new google.maps.Geocoder();    

    marker = new google.maps.Marker({
        map: map,
        draggable: true,
        position: latlng
    });

    google.maps.event.addListener(marker, "dragend", function (event) {
        var point = marker.getPosition();
        map.panTo(point);
    });
    
};

$(document).ready(function () {

    initialize();

    $(function () {
        $(PostCodeid).autocomplete({
            //This bit uses the geocoder to fetch address values
            source: function (request, response) {
                geocoder.geocode({ 'address': request.term }, function (results, status) {
                    response($.map(results, function (item) {
                        return {
                            label: item.formatted_address,
                            value: item.formatted_address
                        };
                    }));
                });
            }
        });
    });

    $('#findbutton').click(function (e) {
        var address = $(PostCodeid).val();
        geocoder.geocode({ 'address': address }, function (results, status) {
            if (status == google.maps.GeocoderStatus.OK) {
                map.setCenter(results[0].geometry.location);
                marker.setPosition(results[0].geometry.location);
                $(latval).val(marker.getPosition().lat());
                $(longval).val(marker.getPosition().lng());
                $("input.latTextBox").val(marker.getPosition().lat());
                $("input.longTextBox").val(marker.getPosition().lng());
            } else {
                alert("Geocode was not successful for the following reason: " + status);
            }
        });
        e.preventDefault();
    });

    //Add listener to marker for reverse geocoding
    google.maps.event.addListener(marker, 'drag', function () {
        geocoder.geocode({ 'latLng': marker.getPosition() }, function (results, status) {
            if (status == google.maps.GeocoderStatus.OK) {
                if (results[0]) {
                    //$('#address').val(results[0].formatted_address);
                    $(latval).val(marker.getPosition().lat());
                    $(longval).val(marker.getPosition().lng());
                    $("input.latTextBox").val(marker.getPosition().lat());
                    $("input.longTextBox").val(marker.getPosition().lng());
                }
            }
        });
    });

});

Lastly…

After adding any resources you need mark them in the editor as a WebResource so they get included in the binary, add the following two lines above the 'namespace Umbraco.Cms.Web.PropertyEditors.GoogleMap' line in your 'GoogleMapEditor.cs' file

[assembly: WebResource("Umbraco.Cms.Web.PropertyEditors.GoogleMap.Resources.gMap.js", "application/x-javascript")]
[assembly: WebResource("Umbraco.Cms.Web.PropertyEditors.GoogleMap.Resources.gMap.css", "text/css", PerformSubstitution = true)]

AGAIN REMEMBER: Now we are done, always mark ALL your resources (And custom views) as an embedded resource (Right click, properties, build action = embedded resource)

See It In Action

Ok now we have our Property Editor pretty much done, you just need to build the site (Actually just click the run button to fire it up) add it to your site as you would do a datatype in a normal Umbraco site (In the developer section).  Add the GoogleMap Property Editor to the 'Homepage' document type and name it 'gMap', save and when you view the admin UI you should see the following:

example-backend

Now type and address or postcode and you should start to see the autocomplete kick in, once you have selected an address click find to geoCode it - And to fine tune the position drag the marker around to suit. Then save and publish.

Using In the Front End

Getting the values out is very easy in the front end, you just use the following extension method

Model.Field<string>("PropertyAlias", "ValueAlias")

Where property Alias is the actual alias of the property we added to the DocType (In our case 'gMap') and 'ValueAlias' is the name of the properties you added in your EditorModel (Ours are 'Postcode, Long & Lat'). So to use these in the front end, we'll just throw the map in the 'Homepage' template.

Open up the template and at the top, under 'ViewBag.Title' add the following

    var Long = Model.Field<string>("gMap", "Long");
    var Lat = Model.Field<string>("gMap", "Lat");

And now above the first H2 title add the following:

<h2>Google Map</h2>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript">
    function initialize() {
        var myLatlng = new google.maps.LatLng(@Html.Raw(Lat.ToString()), @Html.Raw(Long.ToString()));
        var myOptions = {
            zoom: 13,
            center: myLatlng,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        }

        var map = new google.maps.Map(document.getElementById("contactmap"), myOptions);

        var homestring = 'Your Location';
        var homeinfowindow = new google.maps.InfoWindow({ content: homestring });
        var homemarker = new google.maps.Marker({
            position: myLatlng,
            map: map
        });
        google.maps.event.addListener(homemarker, 'click', function () {
            homeinfowindow.open(map, homemarker);
        });

    }

    $(document).ready(function () {
        initialize();
    });
</script>
<div id="contactmap" style="width:400px; height: 300px; border:2px #ccc solid;"></div>

Tada..

example-font-end

As you can see its very simple, and maybe not how you would do it in a real site but it shows you how to get your values back out in a view - I hope this blog post helps you get started playing with property editors, and please let me know your comments below, and if I have missed or got something wrong.

Extending Further…

In the next post we'll be extending this property editor by adding pre-values, manipulating the content before we save it and a few other things.

Happy Coding Smile

Back to top