Sitecore: Linking to Sitecore editor from an .aspx page

This tip comes from Thomas Stern, who got the tip from the Sitecore support team. I am not very well founded in the Sitecore XAML development, so I often find myself using an .aspx page to extend the Sitecore shell.
And sometimes I need my own .aspx page to open the Sitecore editor at a certain Sitecore item. I thought that was impossible, untill Thomas Stern showed me how to do that.

In order to run the Sitecore content editor from an .aspx page, you need to be logged into Sitecore in the shell domain. Then you call the Sitecore client like this:

/sitecore/shell/Applications/Content Manager/Default.aspx?fo={AA7D559F-ABA5-8F6A-ABCD-0ABDF7E74EE1}

This opens the Sitecore editor at the item specified in the “fo” parameter.
There are more parameters:

/sitecore/shell/Applications/Content%20Manager/default.aspx?fo={id}[&la={language}][&vs={version}]&mo=preview
  • fo = The item to open the Sitecore editor at
  • la = The language to open
  • vs = The version to open
  • mo=preview. This parameter removes the content tree at the left side.

To open the Sitecore editor in a new window by clicking a button from your own .aspx page, you can do the following: Create an ordinary asp:Button control and extend the OnClientClick property:

Item myItem = MySitecoreItemToOpen();
myButton.OnClientClick = "javascript:window.open('/sitecore/shell/Applications/Content Manager/Default.aspx?fo=" + myItem.ID + "'); return false;";

 

Advanced Sitecore Google Maps (WCAG Edition)

Advanced Sitecore Google Maps (WCAG Edition) is a new module for Sitecore. The module provides maps, that are accessible and meet the Section 508 requirements. The module is very easy to use for your Sitecore editors.

Google Maps from Google is a great tool, that you can make to your tool with this module. Users are familiar with Google Maps, from many other sites. They know how to zoom in and out and switch between various map types. There is a possibility to show the geodesic maps, terrain maps, satellite maps and combinations.

It is becoming increasingly more important that websites are accessible. The Advanced Sitecore Google Maps module expands Google Maps features to also be accessible and meet the requirements of WCAG 2.0 level AA and Section 508. This module is the only map solution for Sitecore, that meets the WCAG 2.0 Level AA.

The module provides multiple ways of viewing a Google Map, including several filters and accessibility lists. As a developer, you can develop your own views:

Google Maps view including 3-level filter and accessibility list

Google Maps view including 3-level filter and accessibility list

The module is configured from Sitecore using custom Sitecore editors directly on the item that show the map. You can enter an address, or move the map marker to find the map center:

Create a map in Sitecore

Create a map in Sitecore

When creating a map you have full control over the zoom control, the map types to include (including moon, mars and sky views) and how the map should be displayed. You can even add the overview map and the Google search box to your map.

Markers, polygons and polylines are called “overlays”. These overlays can be grouped, and these groups can be used to filter overlays from the map, or to inherit marker icons. All overlays contain titles, text, alternative text and address information that can be used when presenting the overlay.

Markers are created similar to creating a map, by entering an address or moving a marker to the designated position.

Creating a marker in Sitecore

Creating a marker in Sitecore

Polygons and polylines are created in a similar fashion, simply by clicking on the map untill the polygon or polyline is drawn. The polygon and polyline colors, line thickness and transparency are fully controllable:

Adding a polygon to the overlays collection

Adding a polygon to the overlays collection

In order to use Google Maps, you need to register your URL at Google. If your website has many URL’s, you need to register each of them, including sub-domains (www.mysite.com and mysite.com for example). The module contains a key manager where you add all of your Google Maps keys, and the module automatically finds the correct key:

Multiple Google Maps key management

Multiple Google Maps key management

Click here to read more about the module, or Click here to see a demo of the module.

Google Maps polyline encoding in C#

One of the new features in the Advanced Sitecore Google Maps (WCAG edition) is the ability to draw polylines. Polylines in Google Maps can be drawn using 2 methods: By creating a new GPolyline with the array of coordinates to draw, or by using the GPolyline.fromEncoded function which takes an encoded string of coordinates and renders this as a line.

The encoded polyline function has the advantage of being faster and more compressed compared with feeding the GPolyline with an array of coordinates. However, some of the precision is lost, as the encoding algorithm only uses 5 digit coordinates. 5 digits is still a pretty good precision, so I decided to go with the encoded string.

I was looking for a method of encoded my coordinates in C# when i stumbled upon a solution on the SoulSolutions webpage, where the developer demonstrates a Javascript and C# method to encode and decode polyline coordinates.

This is the C# method re-written. SoulSolutions should be credited for the code. However, his code examples are incomplete, so I rewrote them. First I need a Coordinate class, which contains the latitude and longitude of a coordinate set (I have removed comments and other stuff to compress the code):

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

The following function EncodeCoordinates encodes a list of coordinates into an encoded string:

/// <summary>
/// Encodes the list of coordinates to a Google Maps encoded coordinate string.
/// </summary>
/// <param name="coordinates">The coordinates.</param>
/// <returns>Encoded coordinate string</returns>
public static string EncodeCoordinates(List<Coordinate> coordinates)
{
  int plat = 0;
  int plng = 0;
  StringBuilder encodedCoordinates = new StringBuilder();
  foreach (Coordinate coordinate in coordinates)
  {
    // Round to 5 decimal places and drop the decimal
    int late5 = (int)(coordinate.Latitude * 1e5);
    int lnge5 = (int)(coordinate.Longitude * 1e5);
    // Encode the differences between the coordinates
    encodedCoordinates.Append(EncodeSignedNumber(late5 - plat));
    encodedCoordinates.Append(EncodeSignedNumber(lnge5 - plng));
    // Store the current coordinates
    plat = late5;
    plng = lnge5;
  }
  return encodedCoordinates.ToString();
}
/// <summary>
/// Encode a signed number in the encode format.
/// </summary>
/// <param name="num">The signed number</param>
/// <returns>The encoded string</returns>
private static string EncodeSignedNumber(int num)
{
  int sgn_num = num << 1; //shift the binary value
  if (num < 0) //if negative invert
  {
    sgn_num = ~(sgn_num);
  }
  return (EncodeNumber(sgn_num));
}

/// <summary>
/// Encode an unsigned number in the encode format.
/// </summary>
/// <param name="num">The unsigned number</param>
/// <returns>The encoded string</returns>
private static string EncodeNumber(int num)
{
  StringBuilder encodeString = new StringBuilder();
  while (num >= 0x20)
  {
    encodeString.Append((char)((0x20 | (num & 0x1f)) + 63));
    num >>= 5;
  }
  encodeString.Append((char)(num + 63));
  // All backslashes needs to be replaced with double backslashes
  // before being used in a Javascript string.
  return encodeString.ToString().Replace(@"\", @"\\");
}

Using the encoded string, I can draw a line surrounding Denmark like this:

var line = new GPolyline.fromEncoded({
               color: "#0000FF",
               weight: 5,
               opacity: 0.5,
               points: "kpgiI}{wt@{xeDfxv@ylqC|flAyfyDidPiioBoabD_fkAyjfDooZ{nyCtw_BihCnzlBzbyAl}XehjBfp`B_se@vdgAhdPya_BoabDipHoabDngiAsen@jz}@htcAzqt@itcAnha@|~eBdzh@qqnBf~w@zrlCjkx@fppAy{u@zflA{zRpeuC`zWh`]bmx@}byAlwn@ny{DncNn}nDsxd@uqG",
               levels: "BBBBBBBBBBBBBBBBBBBBBBBBBBBB",
               zoomFactor: 32,
               numLevels: 4 });

The line looks like this using 28 coordinates:

Line drawing using GPolyline.fromEncoded

Line drawing using GPolyline.fromEncoded

Assigning Security to items in Sitecore 6 programatically

When Sitecore 6 came out, Sitecore changed the Security model dramatically. Sitecore now uses the .net security model, and on top of this they have built a set of classes to help with the assigning of roles and members.

You will find the helper classes in the Sitecore.Security namespace:

using Sitecore.Security.Accounts;
using Sitecore.Security.AccessControl;

Access rights to items can be added programatically to any item using the AccessRuleCollection class. The following example demonstates how to apply read access to a role (group) using the AccessRuleCollection:

public void SetAccess(Item item)
{
  Role myRole = Role.FromName("sitecore\\myRole");

  // Get the current accessrules
  AccessRuleCollection accessRules = item.Security.GetAccessRules();

  // Apply read access for the "myRole" to the current item
  // and all it's children
  accessRules.Helper.AddAccessPermission(myRole,
     AccessRight.ItemRead,
     PropagationType.Any,
     AccessPermission.Allow);

  // Write the rules back to the item
  item.Editing.BeginEdit();
  item.Security.SetAccessRules(accessRules);
  item.Editing.EndEdit(); 
}

As you already know, the Item in the function’s parameter must come from the “master” database.

The AddAccessPermission is pretty straight forward. In my example I grant access to a role, but the function will also take a user. Please also note that when retrieving a role or user from the user database, you must prefix the user or role name with the domain name.

The AccessRight class defines which right to apply to the item. If you need to grant more than one right, you will need to add them one by one. Like in this example:

PropagationType pt = PropagationType.Any;
AccessPermission ap = AccessPermission.Allow;
accessRules.Helper.AddAccessPermission(myRole, AccessRight.ItemRead, pt, ap);
accessRules.Helper.AddAccessPermission(myRole, AccessRight.ItemWrite, pt, ap);
accessRules.Helper.AddAccessPermission(myRole, AccessRight.ItemRename, pt, ap);
accessRules.Helper.AddAccessPermission(myRole, AccessRight.ItemCreate, pt, ap);
accessRules.Helper.AddAccessPermission(myRole, AccessRight.ItemDelete, pt, ap);
accessRules.Helper.AddAccessPermission(myRole, AccessRight.WorkflowStateDelete, pt, ap);
accessRules.Helper.AddAccessPermission(myRole, AccessRight.WorkflowStateWrite, pt, ap);
accessRules.Helper.AddAccessPermission(myRole, AccessRight.WorkflowCommandExecute, pt, ap);

The PropagationType enumeration determines which items will be granted the access right. Any means the item and all items inheriting. Descendants applies rights to inheriting children only, and Entity applies right to the item only.

Finally, the AccessPermission enumeration determines whether to grant (allow) or deny the access right.

If you wish to know more about the Sitecore security model, you should read the Sitecore Security API Cookbook (available for members of the Sitecore Developer Network only).

Sitecore 6 addAspxExtension=”false” issue

In my previous post (LinkManager – Working with URL’s in Sitecore 6) I was quite enthusiastic about the addAspxExtension feature, which allows you to disable the .aspx extension in Sitecore URL’s, as long as you provide an 404 handler or use IIS7.

Unfortunately the feature seemes to affect the Sitecore Client negatively, because when enabling it, it is no longer possible to edit Sitecore contents:

addAspxExtension=false error

addAspxExtension=false issue

The problem was found using a Sitecore 6.0.0 rev. 081222 running on a IIS6. If you are running this version of Sitecore, you should not set the addAspxExtension to false if your site requires the Sitecore editor to work. If your installation is a front-end server only, you should have no problems.

LinkManager – Working with URL’s in Sitecore 6

Have you noticed that a page in Sitecore 6 can be accessed through several URL’s? This is an example of the products page on the Sitecore website. The following URL’s are all valid and points to the same page and the same language:

http://www.sitecore.net/en/Products.aspx
http://www.sitecore.net/Products.aspx
http://www.sitecore.net/Products.aspx?sc_lang=en
http://www.sitecore.net/Products
http://www.sitecore.net/?sc_itemid={5D8489BF-419B-4336-B9DA-CA704C682B51}

This can confuse statistical tools like Google Analytics, as the statistical data is collected per URL basis. Sitecore’s own Online Marketing Suite does not have this problem, but all tools that collects data from Javascripts inserted on the page will get confused. It is hard to get a total number of hits for one page, as you have to collect the statistics scattered over many URL’s.

So how can you limit the number of URL’s to one page? The solution lies within the linkManager setting in web.config:

<linkManager defaultProvider="sitecore">
  <providers>
    <clear />
    <add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel"
         addAspxExtension="true"
         alwaysIncludeServerUrl="false"
         encodeNames="true"
         languageEmbedding="asNeeded"
         languageLocation="filePath"
         shortenUrls="true"
         useDisplayName="false" />
  </providers>
</linkManager>

These are the default settings in the web.config, which allows the widest range of available URL’s per page. But this is not always a good thing. The following settings caught my eye:

languageEmbedding="asNeeded"

This setting allows Sitecore to add the language as a part of the URL “as needed”. In practice this means “as the wind blows” because it is very hard to find a pattern for when the language is added and not. My recommentation is to use “always” for multiple language sites, and “never” for single language sites.

Another cool setting is this:

addAspxExtension="true"

Setting this to “false” removes the .aspx extension for all URL’s. In order for this to work you will need to use IIS7 and map all incomming requests to ASP.NET, or write your own 404 error handler that redirects any incomming request to the correct .aspx page.

App_Data folder in a virtual application or directory

This is a common issue when working with webservices in virtual applications in the IIS. Imagine this folder structure:

Website with virtual application

Website with virtual application

The “Website” is the website with it’s App_Data, and “Virtual Application” is a virtual application running a webservice with it’s own App_Data.

To get the physical path to the website’s App_Data folder you write:

HttpContext.Current.Server.MapPath("/App_Data/myfile.xml");

And to get the physical path to the Virtual Application’s App_Data folder you write:

HttpContext.Current.Server.MapPath("~/App_Data/myfile.xml");

The difference lies within the tilde (~) sign, as this defines the relativeness of the path. Adding a “~” to the path tells .NET that this path is relative to where I am.

Save Sitecore Item from .aspx editor

Sitecore is an extendable platform, no doubt about it. For example it allows you to add an external (.aspx) page, to an item:

Advanced Sitecore Google Maps Editor

Advanced Sitecore Google Maps Editor

The page will then show up on your item. But what if you wish to modify the actual Sitecore item from the page? Like in this case:
This is one of the editors for the new Advanced Sitecore Google Maps (WCAG Edition) module. The page allows a user to apply a Google Maps map to a Sitecore item. When the user press “Save map”, the map center and zoom-level is saved to the Sitecore item:

Save map center and zoom to the current Sitecore item

Save map center and zoom to the current Sitecore item

This can only be achieved by hooking into some of Sitecore’s Javascript classes. When the “Save map” button is pressed I hook into the window.parent.scForm Javascript object and call item:load and item:refreshchildren.

All of this can be achieved from code-behind. On the button’s On_Click event I modify the current Sitecore item, and then use RegisterStartupScript to register the Javascript events:

protected void btnSave_Click(object sender, EventArgs e)
{
  // Get the Sitecore item to store map center and zoom level
  Sitecore.Data.Database db = Sitecore.Configuration.Factory.GetDatabase(Request.QueryString["database"]);
  Item item = db.GetItem(Request.QueryString["id"]);
  item.Fields["zoom"] = myZoomLevel;
  item.Fields["center"] = myCenter;
  // Now register the client startup script to be executed
  // When page posts back to save the modified Sitecore item
  SitecoreHelper.RegisterRefreshScript(Page, Request.QueryString["id"]);
}

The final function, SitecoreHelper.RegisterRefreshScript, is my own helper function that will register the startup script:

public static void RegisterRefreshScript(Page page, string id)
{
  StringBuilder message = new StringBuilder();
  message.Append("var parentScForm = window.parent.scForm;" + Environment.NewLine);
  message.Append(string.Format("parentScForm.postRequest('','','','item:load(id={0})');", id) + Environment.NewLine);
  message.Append(string.Format("parentScForm.postRequest('','','','item:refreshchildren(id={0})');", id) + Environment.NewLine);
  if (!page.ClientScript.IsStartupScriptRegistered("RefreshItem"))
  {
    page.ClientScript.RegisterStartupScript(typeof(string), "RefreshItem", message.ToString(), true);
  }
}

Thanks to the Sitecore support team to help me out with this one. BTW: It works with both Sitecore 5 and Sitecore 6.

Adding nbsp to asp:DropDownList title

I was trying to mimmick a tree-list in a drop down box. As wee all know, simpler is better, so I just added a few &nbsp;’s in front of my title before adding the ListItem to the DropDownList:

ListItem listItem = new ListItem("&nbsp;&nbsp;" + Item.Title, item.ID.ToString());
ddCategory.Items.Add(listItem);

But because the ListItem is clever enough to encode the contents of the title field, all I get is:

Added   in drop down failed

Added in drop down failed

How can I overcome this? Simply by using Server.HtmlDecode(). Here is a small function that returns a string of HtmlDecoded &nbsp;’s that can be used in the dropdown:

private string Prefix(int count)
{
  if (count == 0)
    return "";
  StringBuilder sb = new StringBuilder();
  for (int i=0;i<count;i++)
  {
    sb.Append("&nbsp;");
  }
  return Server.HtmlDecode( sb.ToString() );
}

Now I can add the spaces to my ListItem:

ListItem listItem = new ListItem(Prefix(level*4) + item.Title, item.ID.ToString());
ddCategory.Items.Add(listItem);

An the dropdown is a simple treelist:

Drop down with tree-like structure

Drop down with tree-like structure

Automatically create TabIndex in a repeater

In order to conform with the Section 508 Accessibility guidelines, and the WCAG Guidelines, you must provide a tabindex for all of your buttons, checkboxes etc.
So what should you do if you create these elements within a Repeater List Control? Then you don’t know how many buttons, checkboxes etc. you have.

The solution is to create a code-behind property (or function) and then call it using the <%# … %> syntax. Please observe the following example:

<asp:Repeater ID="repGroups" runat="server">
  <HeaderTemplate>
   
<ul>
  </HeaderTemplate>
  <ItemTemplate>
   
	<li>     
     
<div>
        <asp:CheckBox ID="cbGroup" Checked="true" runat="server"
         TabIndex='<%# TabIndex %>' Text='<%# Eval("Title") %>' />
     </div>
   </li>
  </ItemTemplate>
  <FooterTemplate>
   </ul>
  </FooterTemplate>
</asp:Repeater>

Notice that asp:CheckBox uses the TabIndex=’<%# TabIndex %>’ to call a property on the page’s codebehind file. This property simply increases an integer and returns the new value:

private int _tabIndex = 0;

public int TabIndex
{
  get
  {
    _tabIndex++;
    return _tabIndex;
  }
}

The private variable _tabIndex is reset each time the page is called, and the TabIndex property returns a new value each time it is called, giving you tabindexes from 1 to infinite.