Our network guys recently added a proxy server to our network which has caused my Windows Mobile device to no longer sync with Exchange via ActiveSync. I thought that's annoying but its ok I will just set the proxy settings in ActiveSync... To my disapointment ActiveSync doenst have that option!

After some investigation I discovered that if you change the "This computer is connected to:" value to "The Internet" (rather than "Automatic") it works fine!

So in summary, with your device connected, in ActiveSync go to:

File > Connection Settings > This computer is connected to: > The Internet


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

Following on from Steve's excellent post on how to copy one or more properties from one Page Type to another, I decided to share how easy it is to turn his hard work into an EPiServer plug-in so that it will appear in the admin area of the CMS.

First you need to 'decorate' (that's an official term honestly!) your page with the following GuiPlugin attribute:

[GuiPlugIn(DisplayName = "Property Duplicator", 
    Description = "Duplicate Page Type Properties", 
    Area = PlugInArea.AdminMenu, 
    Url = "~/cms/Admin/DuplicatePageTypeProperties.aspx")]

If you're not sure what that actually means, it's as simple as adding it above the Page's class definition in the code behind, I.e.

[GuiPlugIn(DisplayName = "Property Duplicator", 
    Description = "Duplicate Page Type Properties", 
    Area = PlugInArea.AdminMenu, 
    Url = "~/cms/Admin/DuplicatePageTypeProperties.aspx")]
public partial class DuplicatePageTypeProperties : System.Web.UI.Page
{
   ...

What the attribute does is tell EPiServer to add an item to the Admin Menu (under the 'Tools' heading) called "Property Duplicator" (the DisplayName) which when clicked will display the Url provided (in this case the page that Steve created).

And that's essentially it. Rebuild the project and log into the admin area and bingo there it is. Now if you stop there it will work fine, but it wont have the same 'look & feel' as the rest of EPiServer's admin area, so all that's left to do is add a reference to the admin area's css file to the <head> of the page as shown below (NOTE: you'll also need to remove the runat="server" from the <head> tag):

<link rel="stylesheet" type="text/css" 
    href="<%=EPiServer.Global.EPConfig.RootDir%>util/styles/system.css">

NOTE: The above principle can be applied to both EPiServer CMS 4 and 5 sites, however Steve's code will need to be updated to reflect the changed made in the EPiServer API in version 5.
NOTE: The Url provided in the GuiPlugin attribute will need to be updated if the page is not created at ~/cms/Admin/DuplicatePageTypeProperties.aspx 

The full code to make it easier is as follows:

DuplicatePageTypeProperties.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="DuplicatePageTypeProperties.aspx.cs" Inherits="EPiServerSample.cms.admin.DuplicatePageTypeProperties" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1">
    <title>Untitled Page</title>
    <style type="text/css">
        label { font-weight: bold; color: navy; }
        select { margin-top: 0.5em; margin-bottom: 1em;}
    </style>
    <link rel="stylesheet" type="text/css" href="<%=EPiServer.Global.EPConfig.RootDir%>util/styles/system.css">
</head>
<body>
    <h1>EPiServer:  Copy Page Type Properties to Another Page Type</h1>
    <form id="form1" runat="server">
    <div style="width:40%; float:left;">
      <asp:Label ID="Label1" runat="server" AssociatedControlID="lbSourcePageType">Source Page Type:</asp:Label><br />
      <asp:ListBox Width="100%" Height="200" ID="lbSourcePageType" AutoPostBack="true" runat="server" /><br />
      <asp:Label ID="Label2" runat="server" AssociatedControlID="lbSourceProperties">Source Properties:</asp:Label><br />
      <asp:ListBox Width="100%" Height="200" ID="lbSourceProperties" SelectionMode="Multiple" runat="server" />
    </div>
    <div style="width:40%; float:left;">
      <asp:Label ID="Label3" runat="server" AssociatedControlID="lbTargetPageType">Target Page Type:</asp:Label><br />
      <asp:ListBox Width="100%" Height="200" ID="lbTargetPageType" runat="server" AutoPostBack="true" /><br />
      <asp:Label ID="Label4" runat="server" AssociatedControlID="lbTargetProperties">Target Properties:</asp:Label><br />
      <asp:ListBox Width="100%" Height="200" ID="lbTargetProperties" runat="server" />
    </div>
    <div style="clear:both; width:auto; margin-left:auto; margin-right:auto;">
        <asp:Button ID="btnCopyProperties" runat="server" OnClick="btnCopyProperties_Click" Text="Copy Properties -->" />
    </div>
    </form>
</body>
</html>

DuplicatePageTypeProperties.aspx.cs

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using EPiServer.PlugIn;

namespace EPiServerSample.cms.admin
{
    [GuiPlugIn(DisplayName = "Property Duplicator", Description = "Duplicate Page Type Properties", Area = PlugInArea.AdminMenu, Url = "~/cms/Admin/DuplicatePageTypeProperties.aspx")]
    public partial class DuplicatePageTypeProperties : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                EPiServer.DataAbstraction.PageTypeCollection p = EPiServer.DataAbstraction.PageType.List();

                lbSourcePageType.DataSource = p;
                lbSourcePageType.DataValueField = "ID";
                lbSourcePageType.DataTextField = "Name";
                lbSourcePageType.DataBind();

                lbTargetPageType.DataSource = p;
                lbTargetPageType.DataValueField = "ID";
                lbTargetPageType.DataTextField = "Name";
                lbTargetPageType.DataBind();
            }

            lbSourcePageType.SelectedIndexChanged +=
                new EventHandler(this.lbPageType_SelectedIndexChanged);
            lbTargetPageType.SelectedIndexChanged +=
                new EventHandler(this.lbPageType_SelectedIndexChanged);

            btnCopyProperties.Enabled =
                (lbSourcePageType.SelectedIndex != -1) &&
                (lbTargetPageType.SelectedIndex != -1);

        }

        protected void lbPageType_SelectedIndexChanged(object sender, EventArgs e)
        {
            ListBox lb = (ListBox)sender;
            ListBox lbp = null;

            if (object.ReferenceEquals(lb, this.lbSourcePageType)) lbp = lbSourceProperties;
            if (object.ReferenceEquals(lb, this.lbTargetPageType)) lbp = lbTargetProperties;

            lbp.Items.Clear();

            if (!String.IsNullOrEmpty(lb.SelectedValue))
            {
                EPiServer.DataAbstraction.PageType pt = EPiServer.DataAbstraction.PageType.Load(int.Parse(lb.SelectedValue));
                lbp.DataSource = pt.Definitions;
                lbp.DataTextField = "Name";
                lbp.DataValueField = "ID";
                lbp.DataBind();
            }
        }


        protected void btnCopyProperties_Click(object sender, EventArgs e)
        {
            int[] selectedIndices = lbSourceProperties.GetSelectedIndices();
            if (selectedIndices.Length == 0) return;

            EPiServer.DataAbstraction.PageType source =
                EPiServer.DataAbstraction.PageType.Load(int.Parse(lbSourcePageType.SelectedValue));
            EPiServer.DataAbstraction.PageType target =
                EPiServer.DataAbstraction.PageType.Load(int.Parse(lbTargetPageType.SelectedValue));


            foreach (int selectedIndex in selectedIndices)
            {
                EPiServer.DataAbstraction.PageDefinition sdef =
                    EPiServer.DataAbstraction.PageDefinition.Load(int.Parse(
                        lbSourceProperties.Items[selectedIndex].Value));

                EPiServer.DataAbstraction.PageDefinition tdef = new EPiServer.DataAbstraction.PageDefinition();
                tdef.PageTypeID = target.ID;
                tdef.DefaultValueType = sdef.DefaultValueType;
                tdef.DefaultValue = sdef.DefaultValue;
                tdef.EditCaption = sdef.EditCaption;
                tdef.FieldOrder = target.Definitions.Count;
                tdef.HelpText = sdef.HelpText;
                tdef.LanguageSpecific = sdef.LanguageSpecific;
                tdef.LongStringSettings = sdef.LongStringSettings;
                tdef.Name = sdef.Name;
                tdef.Required = sdef.Required;
                tdef.Searchable = sdef.Searchable;
                tdef.Tab = sdef.Tab;
                tdef.Type = sdef.Type;

                tdef.PageTypeID = target.ID;
                tdef.Save();
                target.Definitions.Add(tdef);
                target.Save();

            }
            EPiServer.DataAbstraction.PageType.ClearCache();

            lbPageType_SelectedIndexChanged(lbTargetPageType, new EventArgs());
        }
    }
}

Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

I've just installed EPiServer Mail into an EPiServer CMS 5 SP2 site that is running under .NET 3.5 (yes i know its not officially supported but hey). Much to my annoyance I had 3 errors to fix - which simply meant removing 3 lines from the web.config that either the EPiServer Mail install (or the hotfix) added. The offending lines which must be removed are as follows:

<add assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

<add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

I've just spent too long trying to upgrade EPiServer CMS 5 to SP2 but kept getting the following error:

Login failed for user ''. The user is not associated with a trusted SQL Server connection.

Turns out the problem was that EPiServerManager was pulling the connection string out of my web.config, but it appears it was ignoring the fact it specified an Integrated/Windows connection and was passing an empty SQL username to the database - hence the empty quote marks above. (cheers for your help Steve!)

Solution: Temporarily change the SQL connection string in the site's web.config to use an SQL username/password, perform the update and change the string back.


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

Having stumbled across Google's Webmaster Tools a while ago it's been in the back of my mind to create a custom HttpHandler to dynamically create a (virtual) SiteMap.xml file that Google (and other search engines) can use as a reference when spidering my sites.

I thought I'd share the basic implantation as a starting point. Ideally you'd add a property to each EPiServer PageType page for "change frequency" and "priority" which the sitemap generator would then use, but for this basic version I've simply set the homepage to 1.0 (the maximum priority) with a daily change, and all other pages to 0.6 and weekly.

First declare the class and the required members:

public class SearchEngineSiteMap : IHttpHandler
{
    bool IHttpHandler.IsReusable
    {
        get { return true; }
    }
    void IHttpHandler.ProcessRequest(HttpContext context)
    {
        GenerateSiteMap(context);
    }

Next we need to configure the output stream, create an XmlTextWriter and the outer Xml block:

/// <summary>
/// Generate the SiteMap
/// </summary>
/// <param name="context"></param>
private void GenerateSiteMap(HttpContext context)
{
    //Set the response information
    context.Response.Expires = -1;
    context.Response.ContentType = "application/xml";
    Encoding encoding = new UTF8Encoding();
    context.Response.ContentEncoding = encoding;

    //Create an XMLTextWriter to build the XML, passing it the context's outputstream
    XmlTextWriter xmlTextWriter = new XmlTextWriter(context.Response.OutputStream, encoding);
    xmlTextWriter.Formatting = Formatting.Indented;
    xmlTextWriter.WriteStartDocument();

    //Write the root xml element
    xmlTextWriter.WriteStartElement("urlset");
    xmlTextWriter.WriteStartAttribute("xmlns");
    xmlTextWriter.WriteValue("http://www.sitemaps.org/schemas/sitemap/0.9");
    xmlTextWriter.WriteEndAttribute();

    //Get EPiServer's StartPage (not the RootPage!)
    PageData p = EPiServer.DataFactory.Instance.GetPage(PageReference.StartPage);

    //SiteMaps can only contain unique urls so maintain a list of added urls
    List<string> alreadyAddedUrls = new List<string>();

    //Now call recursive method to populate every published/visible etc page
    RenderNodesToSiteMap(
        context,
        xmlTextWriter,
        alreadyAddedUrls,
        p);

    //Close the root element
    xmlTextWriter.WriteEndElement();
    //end of document
    xmlTextWriter.WriteEndDocument();
    //finally close the XMLTextWriter
    xmlTextWriter.Close();
}

Finally we need to add a method that will be recursively called for each published page in the site:

/// <summary>
/// Recursively converts the given page into XML for use in the sitemap.
/// </summary>
/// <param name="context">Current Context</param>
/// <param name="xmlTextWriter">XMLTextWriter to write give page (p) to</param>
/// <param name="alreadyAddedUrls">List of Urls already added to the SiteMap</param>
/// <param name="p">The page to add to the sitemap</param>
private void RenderNodesToSiteMap(
    HttpContext context,
    XmlTextWriter xmlTextWriter,
    List<string> alreadyAddedUrls,
    PageData p)
{
    //Make sure the page is published
    if (PageDataUtilities.IsPagePublished(child))
    {
        //Get the page's 'Friendly' URL
        string url = PageDataUtilities.GetFriendlyUrl(p, true);

        // Make sure this URL is not in the XML already
        if (!alreadyAddedUrls.Contains(url))
        {
            //Add it ready to check later
            alreadyAddedUrls.Add(url);
            //Write the Url element
            xmlTextWriter.WriteStartElement("url");
            //Add the location (Url) attribute - making sure its encoded!
            xmlTextWriter.WriteElementString("loc", HttpUtility.HtmlEncode(url));
            //Add when it was last modified
            xmlTextWriter.WriteElementString(
                "lastmod",
                p.Changed.ToString("u", CultureInfo.InvariantCulture).Replace(" ", "T"));
            //If its the StartPage set the change frequency to daily
            //and the priority to 1
            if (p.PageLink == PageReference.StartPage)
            {
                xmlTextWriter.WriteElementString(
                "changefreq",
                "daily");
                xmlTextWriter.WriteElementString(
                    "priority",
                    "1.0");
            }
            else //Otherwise weekly and a lower priority
            {
                xmlTextWriter.WriteElementString(
                "changefreq",
                "weekly");
                xmlTextWriter.WriteElementString(
                 "priority",
                 "0.6");
            }
            //Close the URL node
            xmlTextWriter.WriteEndElement();
        }
        //Now loop through all the 
        foreach (PageData child in EPiServer.DataFactory.Instance.GetChildren(p.PageLink))
        {

            RenderNodesToSiteMap(
                context,
                xmlTextWriter,
                alreadyAddedUrls,
                child);

        }
    }

With all that done the last thing is to register it in the Web.config (inside the System.Web element) as follows:

<httpHandlers>
  ...
  <add 
    path="sitemap.xml" 
    verb="*" 
    type="MyLibrary.SearchEngineSiteMap, MyLibrary" />
  ...
</httpHandlers>

Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

As part of my Search Engine friendly SiteMap.xml generator (using a HttpHandler - expect a blog post shortly...) I needed to output the friendly URL for each page in the site. However a call to PageData.LinkURL returns the actual link to the page (example shown below), not a "friendly" one that gets displayed in the browser address bar, or in any Hyperlink within the site.

/MySite/Default.aspx?id=26&epslanguage=en-GB

I had a look through the EPiServer documentation and couldn't find anything obvious so resorted to creating my own method to do it (shown below). Simply pass the relevant PageData object to the function and it will return the page's Friendly URL. e.g. /mysite/en-GB/about-us

/// <summary>
/// Get a friendly URL for the given PageData object
/// </summary>
/// <param name="pd">The page to get the Friendly URL from</param>
/// <param name="Absolute">Return an absolute path</param>
/// <returns>The friendly Url for the given PageData object</returns>
public static string GetFriendlyUrl(PageData pd, bool Absolute)
{
    UrlBuilder url = new UrlBuilder(pd.LinkURL);
    //Call UrlRewriteProvider's ConvertToExternal method
    EPiServer.Global.UrlRewriteProvider.ConvertToExternal(
       url,
       pd.PageLink,
       UTF8Encoding.UTF8);
    if (!Absolute)
        return url.ToString();
    else
        return StringUtilities.GetBaseUrl() + url.ToString();           
}

If you want to use Absolute paths you'll need to add this method:

/// <summary>
/// Gets the base URL from the current Context.
/// </summary>
/// <returns></returns>
public static string GetBaseUrl()
{
    //First get the current port
    string Port = HttpContext.Current.Request.ServerVariables["SERVER_PORT"];
    if (Port == null || Port == "80" || Port == "443")
        Port = "";
    else //Its not a standard port so add it
        Port = ":" + Port;
    //Now get the protocol (http or https)
    string Protocol = HttpContext.Current.Request.ServerVariables["SERVER_PORT_SECURE"];
    if (Protocol == null || Protocol == "0")
        Protocol = "http://";
    else
        Protocol = "https://";
    //Finally combine the protocol, sever name and port
    string url = Protocol + HttpContext.Current.Request.ServerVariables["SERVER_NAME"] + Port;
    //EPiServer's url start with a / so remove the url if (when) it contains one
    if (url.EndsWith("/"))
        return url.Remove(url.LastIndexOf("/"));
    else
        return url;
}

Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

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

Following on from my Google Analytics server control, I wanted make something "cooler", so creating a Server Control to render a Google Map control seemed ideal. Part 1 covers the creation of the this control, stay tuned for part 2 which will focus on creating a custom EPiServer property so it can be used in the EPiServer edit area, featuring Geocoding to lookup the coordinates from a UK post code, Map type selection and zoom level.

Anyway here's the generic bit that can be used on any site...

First off we need a set of basic properties to hold Latitude, Longitude, Zoom Level and Map Type:

public double Latitude
{
    get { return GetPropertyValue("Latitude", (double)0); }
    set { SetPropertyValue("Latitude", value); }
}
public double Longitude
{
    get { return GetPropertyValue("Longitude", (double)0); }
    set { SetPropertyValue("Longitude", value); }
}
public int Zoom
{
    get { return GetPropertyValue("Zoom", 5); }
    set { SetPropertyValue("Zoom", value); }
}
public string MapType
{
    get { return GetPropertyValue("MapType", "Map"); }
    set { SetPropertyValue("MapType", value); }
}

Next come's the rather complex looking JavaScript code. I've done it so that it will allow multiple controls to be placed on the same page. IMPORTANT: Make sure you {0} in COMMONJAVASCRIPT is your unique Google Maps/API Key, which is generated on a per domain basis.

private const string COMMONJAVASCRIPT = @"
        <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={0}"" type=""text/javascript""></script>";
        
        private const string JAVASCRIPT = @"
        <script type=""text/javascript"">
        function GoogleMapLoad{6}() {{
          if (GBrowserIsCompatible()) {{
            var map = new GMap2(document.getElementById(""{3}""));
        
                if(""{5}"" == ""Hybrid"")
                   map.setCenter(new GLatLng({1},{2}), parseInt({4}), G_HYBRID_MAP);
                else if(""{5}"" == ""Satellite"")
                   map.setCenter(new GLatLng({1},{2}), parseInt({4}), G_SATELLITE_MAP);
                else
                   map.setCenter(new GLatLng({1},{2}), parseInt({4}), G_NORMAL_MAP);
                
                var point = new GLatLng({1},{2});
                map.addOverlay(new GMarker(point));
                map.addControl(new GMapTypeControl());
                map.addControl(new GLargeMapControl());
            }}
        }}
        </script>";

Next we need to override the default tag that the server control would typically render (span) with a div, so that the Google map has a "canvas".

//Google map must be rendered inside a <DIV> tag, so we'll override the default <SPAN>.
protected override HtmlTextWriterTag TagKey
{
    get
    {
        //Return DIV not SPAN
        return HtmlTextWriterTag.Div;
    }
}

And last but not least, the overloaded CreateChildControls(). Firstly we check to see if there are (valid) Latitude and longitude values, if there is we register two blocks of JavaScript. Notice the second block's key includes the controls ID - which allows multiple maps to be used on the same page. The first 'common' block will only be registered once regardless of how many maps on the page. The second block has five strings passed to it which include the Google API Key (stored in the AppSettings) the control's ClientID (so the map's canvas can be accessed) along with the lat/lng/zoom etc.

protected override void CreateChildControls()
{
    if (Latitude != 0 && Longitude != 0)
    {
        //Check if the COMMON google map javascript block has been registered
        if (!Page.ClientScript.IsClientScriptBlockRegistered(typeof(GoogleMap), "GoogleMap"))
            Page.ClientScript.RegisterClientScriptBlock(typeof(GoogleMap), "GoogleMap", COMMONJAVASCRIPT);

        //Check if the google map javascript block for THIS map has been registered
        if (!Page.ClientScript.IsClientScriptBlockRegistered(typeof(GoogleMap), "GoogleMap" + ID))
            Page.ClientScript.RegisterClientScriptBlock(typeof(GoogleMap), "GoogleMap" + ID, 
                string.Format(CultureInfo.InvariantCulture, JAVASCRIPT,
                        WebConfigurationManager.AppSettings["GoogleMapKey"], //0
                        Latitude,  //1
                        Longitude, //2
                        this.ClientID, //3
                        Zoom, // 4
                        MapType, //5
                        ClientID //6),