This post tells you how to take what I discussed in Part 1 and turn it into a custom EPiServer property, so CMS user's can simply enter a postcode and it will call of to Google's Local Search API, return the Coordinates for the postcode and plot a marker on the map. The user can then drag the marker for a more exact location, set the zoom level and finally the type of map (Normal, Satellite or Hybrid). The data is then rendered on the public site using the code from Part 1. The screenshot below show's how it will look in EPiServer (click the thumbnail for the full size version):

GoogleMapEPiServer

Creating a custom EPiServer property is relatively easy - although I think I've not quite got the hang of how to store the actual data. It must be converted to a string for storage in the database. I started to use an XmlSerializer but got confused (mainly as the JavaScript was doing my head in) as to where i should be doing the serialisation. Sadly the only example i could find on the net was how to create a custom property that was a string - about as much use as a chocolate fire guard! So here's my way of doing it - using a comma separated string (dirty I know!) - please feel free to show me the correct way.

EPiServer properties are split into two parts, the data and UI aspects. The first is how the data is persisted into the database, the second controls how the data is rendered to the page for both the public and on the edit page in the CMS.

1) CoordinateProperty.cs

Firstly mark your class with the following attribute:

[Serializable]
[PageDefinitionTypePlugIn(
    DisplayName = "Coordinate", 
    Description = "Coordinate for use with (Google) Map Control")]
public class CoordinateProperty : PropertyData

Next some properties to help control the map:

string _string;

public double? Latitude
{
    get
    {
        if (!string.IsNullOrEmpty(String) && String.Contains(','))
        {
            double temp = 0;
            if (double.TryParse(String.Split(',')[0].Trim(), out temp))
                return temp;
            else
                return null;
        }
        else
            return null;
     }
 }
public double? Longitude
{
    get
    {
        if (!string.IsNullOrEmpty(String) && String.Contains(','))
        {
            double temp = 0;
            if (double.TryParse(String.Split(',')[1].Trim(), out temp))
                return temp;
            else
                return null;
        }
        else
            return null;
    }
}
public int Zoom
{
    get
        {
        if (!string.IsNullOrEmpty(String) && String.Contains(','))
        {
            int temp = 0;
            if (int.TryParse(String.Split(',')[2].Trim(), out temp))
                return temp;
            else
                return 5;
         }
         else
            return 5;
    }
}
public string MapType
{
    get
    {
        if (!string.IsNullOrEmpty(String) && String.Contains(','))
            return String.Split(',')[3].Trim();
        else
            return "Map";
    }
}

Next the implementation for the abstract members of PropertyData.

public override PropertyDataType Type
{
    get { return PropertyDataType.LongString; }
}
public override Type PropertyValueType
{
    get { return this.GetType(); }
}
public override object Value
{
    get
    {
        if (this.IsNull)
            return null;
        else
            return this.String;
    }
    set
    {
        base.ThrowIfReadOnly();
        base.SetPropertyValue(value, delegate
        {
            this.String = value.ToString();
        });
    }
}
[XmlIgnore]
protected virtual string String
{
    get { return this._string; }
    set
    {
        base.ThrowIfReadOnly();
        if (PropertyData.QualifyAsNullString(value))
            base.Clear();
        else if ((this._string != value) || (this.IsNull))
        {
            this._string = value;
            base.Modified();
        }
    }
}
public override PropertyData ParseToObject(string str)
{
    XmlSerializer SerializerObj = new XmlSerializer(this.GetType());
    using (TextReader stream = new StringReader(str))
    {
        return (CoordinateProperty)SerializerObj.Deserialize(stream);
    }
}
public override void ParseToSelf(string str)
{
    String = str;
}
protected override void SetDefaultValue()
{
    this.String = ",,5,Map";
}
public override IPropertyControl CreatePropertyControl()
{
    return new CoordinatePropertyControl();
}

2) CoordinatePropertyControl.cs

With that done we now need to control how the above class is rendered. First we need to add the four Textboxes:

TextBox latitiude;
TextBox longitude;
TextBox zoom;
TextBox mapType;

Next some basic methods, the first just makes it easier further down the line, the second is required:

/// <summary>
/// Gets the CoordinateProperty instance for this IPropertyControl.
/// </summary>
/// <value>The property that is to be displayed or edited.</value>
public CoordinateProperty CoordinateProperty
{
    get { return PropertyData as CoordinateProperty; }
}
/// <summary>
/// Does the property support in page edit
/// </summary>
public override bool SupportsOnPageEdit
{
    get { return false; }
}

Next we need to provide code to display the Google Map in VIEW (public) mode, here we'll simply use the class we created in Part 1 of this post:

/// <summary>
/// Inherited. Create a controls for rendering the property in view mode.
/// </summary>
public override void CreateDefaultControls()
{
    //Check if the Latitude and Longitude have values
    if (this.CoordinateProperty.Latitude.HasValue && this.CoordinateProperty.Longitude.HasValue)
    {
        GoogleMap map = new GoogleMap();
        map.ID = "map";
        if (Width.IsEmpty)
            map.Width = new Unit(100, UnitType.Percentage);
        else
            map.Width = Width;
        if (Height.IsEmpty)
            map.Height = new Unit(400, UnitType.Pixel);
        else
            map.Height = Height;

        map.Latitude = this.CoordinateProperty.Latitude.Value;
        map.Longitude = this.CoordinateProperty.Longitude.Value;
        map.Zoom = this.CoordinateProperty.Zoom;
        map.MapType = this.CoordinateProperty.MapType;
        Controls.Add(map);
    }
}

Right that's the basics out of the way, now its time to control how the property is rendered in EDIT mode in the CMS, but first we need to add the JavaScript:

private const string JAVASCRIPT = @"
<script src=""http://maps.google.com/maps?file=api&amp;v=2&amp;key={0}"" type=""text/javascript""></script>
<script src=""http://www.google.com/uds/api?file=uds.js&amp;v=1.0&amp;key={1}"" type=""text/javascript""></script>
<script type=""text/javascript"">
var map;
var localSearch = new GlocalSearch();

function usePointFromPostcode(postcode, callbackFunction) {{
    localSearch.setSearchCompleteCallback(null, 
        function() {{
            if (localSearch.results[0])
            {{        
                var resultLat = localSearch.results[0].lat;
                var resultLng = localSearch.results[0].lng;
                var point = new GLatLng(resultLat,resultLng);
                callbackFunction(point, 17);
            }}else{{
                alert(""Postcode not found!"");
            }}
        }});    
    localSearch.execute(postcode + "", UK"");
}}

function mapLoad() {{
    if (GBrowserIsCompatible()) {{
    //check map not already created!
        if(!map)
        {{
        map = new GMap2(document.getElementById(""{5}""));
        map.addControl(new GLargeMapControl());
        map.addControl(new GMapTypeControl());

        var mType = document.getElementById(""{7}"").value;
        if(mType == ""Hybrid"")
            map.setCenter(new GLatLng(54.622978,-2.592773), 5, G_HYBRID_MAP);
        else if(mType == ""Satellite"")
            map.setCenter(new GLatLng(54.622978,-2.592773), 5, G_SATELLITE_MAP);
        else
            map.setCenter(new GLatLng(54.622978,-2.592773), 5, G_NORMAL_MAP);

        GEvent.addListener(map, ""zoomend"", function(oldLevel, newLevel) {{
            //alert(""Old Zoom: "" + oldLevel + "" New Level: "" + newLevel);
            document.getElementById(""{6}"").value = newLevel;
        }});

        GEvent.addListener(map, ""maptypechanged"", function() {{
            document.getElementById(""{7}"").value = map.getCurrentMapType().getName();
            document.getElementById(""{6}"").value = map.getZoom();
        }});
        
        }}
    }}
}}

function addLoadEvent(func) {{
  var oldonload = window.onload;
  if (typeof window.onload != 'function') {{
    window.onload = func;
  }} else {{
    window.onload = function() {{
      oldonload();
      func();
    }}
  }}
}}

function addUnLoadEvent(func) {{
    var oldonunload = window.onunload;
    if (typeof window.onunload != 'function') {{
      window.onunload = func;
    }} else {{
      window.onunload = function() {{
        oldonunload();
        func();
      }}
    }}
}}

function placeMarkerAtPoint(point, zoom)
{{
    if(!map)
    {{
        mapLoad();
    }}
    var marker = new GMarker(point, {{draggable: true}});
    GEvent.addListener(marker, ""dragstart"", function() {{
        map.closeInfoWindow();
    }});
    GEvent.addListener(marker, ""dragend"", function() {{
        LoadCoordinates(marker.getPoint(), document.getElementById('{2}'), document.getElementById('{3}'));
        marker.openInfoWindowHtml(""Lat: "" + marker.getPoint().lat() + ""<br />Lng: "" + marker.getPoint().lng());
    }});
    map.clearOverlays();
    map.addOverlay(marker);
    setCenterToPoint(point, zoom);
    LoadCoordinates(point, document.getElementById('{2}'), document.getElementById('{3}'));
}}

function setCenterToPoint(point, zoom)
{{
    map.setCenter(point, parseInt(zoom));
}}

function showPointLatLng(point)
{{
    alert(""Latitude: "" + point.lat() + ""\nLongitude: "" + point.lng());
}}
function LoadCoordinates(point, txtLat, txtLng)
{{
    txtLat.value = point.lat();
    txtLng.value = point.lng();
}}

addLoadEvent(mapLoad);
addUnLoadEvent(GUnload);
</script>";

private const string EXISTING = @"
<script type=""text/javascript"">
    placeMarkerAtPoint(new GLatLng({0},{1}),{2});
</script>";
/// <summary>
/// Create the controls needed to edit the property
/// </summary>
public override void CreateEditControls()
{
    //Create controls required to edit the property
    latitiude = new TextBox();
    longitude = new TextBox();
    zoom = new TextBox();
    mapType = new TextBox();
    Label lLat = new Label();
    Label lLong = new Label();
    Label lZoom = new Label();
    Label lMapType = new Label();
    Literal brk1 = new Literal();
    Literal brk2 = new Literal();
    Literal brk3 = new Literal();
    Literal brk4 = new Literal();
    Label lPost = new Label();
    TextBox txtPostCode = new TextBox();
    Button btn = new Button();
    Panel Div = new Panel();

    //Set the line break text.
    brk1.Text = "<br />";
    brk2.Text = "<br />";
    brk3.Text = "<br />";
    brk4.Text = "<br />";

    //Set the Latitude TextBox properties
    latitiude.ID = "latitiude";
    latitiude.Style.Add(System.Web.UI.HtmlTextWriterStyle.MarginLeft, "20px");
    latitiude.Width = new Unit(140, UnitType.Pixel);

    //Set the Longitude TextBox properties
    longitude.ID = "longitude";
    longitude.Style.Add(System.Web.UI.HtmlTextWriterStyle.MarginLeft, "20px");
    longitude.Width = new Unit(140, UnitType.Pixel);

    //Set the Postcode TextBox properties
    txtPostCode.ID = "postcode";
    txtPostCode.Width = new Unit(140, UnitType.Pixel);
    txtPostCode.Style.Add(System.Web.UI.HtmlTextWriterStyle.MarginLeft, "20px");
    txtPostCode.Style.Add("text-transform", "uppercase");

    //Set the MapType TextBox properties
    mapType.ID = "maptype";
    mapType.Style.Add(System.Web.UI.HtmlTextWriterStyle.MarginLeft, "20px");
    mapType.Width = new Unit(65, UnitType.Pixel);

    //Set the Zoom TextBox properties
    zoom.Style.Add(System.Web.UI.HtmlTextWriterStyle.MarginLeft, "20px");
    zoom.Width = new Unit(65, UnitType.Pixel);

    //Set the latitude label properties
    lLat.Text = "Latitude";
    lLat.Width = new Unit(60, UnitType.Pixel);
    lLat.Style.Add("float", "left");

    //Set the longitude label properties
    lLong.Text = "Longitude";
    lLong.Width = new Unit(60, UnitType.Pixel);
    lLong.Style.Add("float", "left");

    //Set the map type label properties
    lMapType.Text = "Map Type";
    lMapType.Width = new Unit(60, UnitType.Pixel);
    lMapType.Style.Add("float", "left");

    //Set the postcode label properties
    lPost.Text = "Post Code";
    lPost.Width = new Unit(60, UnitType.Pixel);
    lPost.Style.Add("float", "left");

    //Set the zoom label properties
    lZoom.Text = "Zoom";
    lZoom.Width = new Unit(60, UnitType.Pixel);
    lZoom.Style.Add("float", "left");
                    
    //Set the button properties
    btn.Text = "Look Up";
    
    //Set the map canvas
    Div.ID = "mapDiv";
    Div.Style.Add(System.Web.UI.HtmlTextWriterStyle.MarginTop, "20px");
    Div.Width = new Unit(400, UnitType.Pixel);
    Div.Height = new Unit(400, UnitType.Pixel);

    this.ApplyControlAttributes(latitiude);
    this.ApplyControlAttributes(longitude);
    this.ApplyControlAttributes(zoom);

    //Add the controls to the controls collection
    Controls.Add(lPost);
    Controls.Add(txtPostCode);
    Controls.Add(btn);
    Controls.Add(brk1);
    Controls.Add(lLat);
    Controls.Add(latitiude);
    Controls.Add(brk2);
    Controls.Add(lLong);
    Controls.Add(longitude);
    Controls.Add(brk3);
    Controls.Add(lZoom);
    Controls.Add(zoom);
    Controls.Add(brk4);
    Controls.Add(lMapType);
    Controls.Add(mapType);
    Controls.Add(Div);

    //Add the JavaScript required to render the google map
    if (!Page.ClientScript.IsClientScriptIncludeRegistered(typeof(CoordinatePropertyControl), "MAP"))
        Page.ClientScript.RegisterClientScriptBlock(typeof(CoordinatePropertyControl), "MAP", 
            string.Format(JAVASCRIPT,
            WebConfigurationManager.AppSettings["GoogleMapKey"], //0
            WebConfigurationManager.AppSettings["GoogleMapKey"], //1
            latitiude.ClientID, //2
            longitude.ClientID, //3
            "", //4
            Div.ClientID, //5
            zoom.ClientID, //6
            mapType.ClientID, //7
            PropertyData.Name //8
            ));

    //Add the onclick javascript to the postcode
    //lookup button
    btn.Attributes.Add("onclick", 
        string.Format("usePointFromPostcode(document.getElementById('{0}').value, placeMarkerAtPoint); return false;",
        txtPostCode.ClientID));

    //Make the map type and zoom textboxes
    //readonly as they are set by the google
    //map control.
    zoom.Attributes.Add("readonly", "true");
    mapType.Attributes.Add("readonly", "true");

    //Lastly populate the edit controls with values from the underlying CoordinateProperty
    this.SetupEditControls();
}

With that done we need to override the method which will populate the edit controls with values from the underlying CoordinateProperty (either default of from the database)

/// <summary>
/// Inherited. Initialize the value of the TextBox control.
/// </summary>
protected override void SetupEditControls()
{
    //Set the zoom and map type textbox values
    zoom.Text = CoordinateProperty.Zoom.ToString();
    mapType.Text = CoordinateProperty.MapType;

    //If the Latitude/Longitude have values set the relevant
    //textbox properties and add the GoogleMap Javascript
    if (CoordinateProperty.Latitude.HasValue && CoordinateProperty.Longitude.HasValue)
    {
        latitiude.Text = CoordinateProperty.Latitude.Value.ToString();
        longitude.Text = CoordinateProperty.Longitude.Value.ToString();

        if (!Page.ClientScript.IsStartupScriptRegistered(typeof(CoordinatePropertyControl), 
            "EXISTING"))
        {
            Page.ClientScript.RegisterStartupScript(typeof(CoordinatePropertyControl), 
                "EXISTING",
                string.Format(EXISTING,
                    latitiude.Text,
                    longitude.Text,
                    zoom.Text));
        }
    }
}

Finally we must override the method which sets the value of the underlying CoordinateProperty with values entered in the UI by the user:

/// <summary>
/// Inherited. Applies changes for the posted data to the page's properties when the RenderType property
/// is set to Edit.
/// </summary>
public override void ApplyEditChanges()
{
    base.SetValue(string.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3}", this.latitiude.Text, this.longitude.Text, this.zoom.Text, this.mapType.Text));
}

And that's it - put it all together and you'll be able to configure a Google Map 'widget' in EPiServer.

Oh - one small thing to note - the above JavaScript will only allow for ONE CoordinateProperty to be added to a PageType. If you add more than one to the same PageType it will break. If I get time I'll change it, but our current requirement is only for one map per page - sorry!


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist
posted @ Monday, June 09, 2008 10:24 AM | in C# EPiServer .NET ASP.NET AJAX ASP.NET

Comments

Gravatar
# re: C#: Google Map Server Control - Part 2 - Custom EPiServer Property
Posted by amila
on 7/13/2008 12:22 PM
sef

Post Comment

Title *
Name *
Email
Url
Comment *  


Please add 6 and 2 and type the answer here: