Frederik Vig – ASP.NET developer

Follow me

Posts tagged ‘EPiServer’

Part 1: Setting up the development environment – Create an EPiServer site from scratch

In this first part I’m taking you through setting up everything on a new computer. Most likely you’ll have done some of this before, in that case feel free to skip to the parts that interest you. If all this is new to you, I would install everything in the order described.

Update 08.12.2009

Ted Nyberg posted a nice blog post: How to set up an EPiServer website, that you should definitely check out (after you’ve read this one of course :) ).

Installing IIS, ASP.NET, SQL Server 2008, and Visual Studio

Microsoft as a great tool for quickly installing everything you need, its called the Web Platform Installer. Download and install it, after its installed, launch it and choose Web Platform. Here you’ll be able to choose the web server, frameworks and runtimes, and database server, that you’ll need.

Web Platform Installer

For more information about the Web Platform Installer see Scott Guthrie’s introduction post and the official site.

I’m using Visual Studio 2008 Professional in this series, but you can use any version you like, including the free Visual Web Developer 2008.

Installing EPiServer

Go to EPiServer World and download the latest version of EPiServer, I’m using EPiServer CMS 6.

Unzip, and click setup to start. When asked choose Application Files (we’re adding the site later).

Choose Application Files

Subversion

Subversion is a popular open source version control system. It will help us get the latest version of our code, track changes, and a whole lot of other things. For more information I recommend taking a look at the free book: Version Control with Subversion.

Throughout this series I’ll update and keep all the code in EPiCode’s subversion repository. To work with EPiCode’s repository we need a Subversion client. My favorite is TortoiseSVN, but you can use any you like. You’ll need to download and install it (or any other subversion client). After the installation and restart you should have a few new options in Windows Explorer.

TortoiseSVN shell extension in Windows Explorer

When following this series I recommend building your own project, and just use the project on EPiCode for reference when you’re stuck. Or just want to copy some files/folders.

To do a checkout of the repository, right click in Windows Explorer inside the folder you’d like to place the code, and choose SVN Checkout. Type in the URL https://www.coderesort.com/svn/epicode/JungleLand (note: you need to register on EPiCode first, if you don’t already are).

SVN Checkout of JungleLand from EPiCode

Other tools

My main development browser is Firefox, the reason for that is that you have access to a ton of great extensions that’ll help you a lot when developing a new site. One of those extensions is Firebug. Download and install both Firefox and Firebug if you don’t already have them. For more information, take a look at this screencast: Introduction to Firebug.

With Visual Studio I have two other tools: VisualSVN and ReSharper. VisualSVN to help me with Subversion, and ReSharper for helping me with the code. You don’t need all these tools to follow along with this series, but I recommend trying them out.

Also install UnleashIt. UnleashIt is used for removing subversion files (hidden files that Subversion uses to keep track), code-behind, class files etc, when deploying to our production server (everything is compiled inside .dll files).

The last program we need is Reflector. This is one of my favorite tools when working with EPiServer. It’ll allow you to open up any .dll file and inspect the code it contains. This is great for when you’re stuck on a problem and need to see how something behaves, or for just having fun and peeking inside EPiServer :) .

Reflector in action

Installing the site

Start the deployment center: go to Start, EPiServer, and EPiServer Deployment Center. Choose the EPiServer version you’d like to install (I’m using 6.0.382.1), and click, “Install site and SQL Server database”.

Step 1

Nothing special here, I’m using http://jungle as my local site url – for that to work I need to update my hosts file, located under C:\Windows\System32\drivers\etc\hosts (edit it with notepad or any other code editor). I’ve added the line: 127.0.0.1 jungle

Step 2

If you have a local SQL Server Express instance you can most likely just type .\SQLEXPRESS. If that doesn’t work, open up services (under Administrative tools in control panel) and check what your local SQL Server instance is called.

If you have other problems connecting, make sure that TCP/IP is enabled for your SQL Server instance. Open up Configuration Manager: go to Start, SQL Server 2008, Configuration tools, and SQL Server Configuration Manager.

Step 3

I just used the default VPP path suggested.

Step 4

Make sure to install the Public Templates with only the English content.

Step 5

If you like you can order a developer license, I’m just going to continue without adding a license file.

Step 6

Great, we’re now ready to install everything!

Time for a little coffee break – when we come back everything should be installed!

After installation

When the installation completes you should see something like this in your default browser.

Project folder structure

Great, we now have most of our environment configured and setup. Lets go to our projects root folder (C:\EPiServer\Sites\JungleSite) and rename PublicTemplates.csproj to JungleSite.csproj. Then open it in Visual Studio (double-click on JungleSite.csproj). You should have a structure similar to this:

Lets expand the Templates folder, and add a new folder called Jungle. Under Jungle, add the following folders:

  • Images
  • MasterPages
  • Pages
  • Scripts
  • Styles
  • Units
  • Util

Next step is deleting the App_Data and Service_References folders. After you’ve done that expand Properties and open up AssemblyInfo.cs, and update the information here.

// General Information about an assembly is controlled through the following 
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("JungleSite")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Frederik Vig")]
[assembly: AssemblyProduct("JungleSite")]
[assembly: AssemblyCopyright("� 2009-2010 by Frederik Vig. All rights reserved")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
 
// Setting ComVisible to false makes the types in this assembly not visible 
// to COM components.  If you need to access a type in this assembly from 
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
 
// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version 
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Revision and Build Numbers 
// by using the '*' as shown below:
[assembly: AssemblyVersion("1.0.0.0")]

Last step is renaming the assembly and default namespace. Right click JungleSite and choose Properties.

Make sure it builds by pressing either ctrl + shift + b, or going to: Build, Build JungleSite. It should build fine (since we didn’t change any of the code). Last thing to remember is to delete the old dll file. Go to your bin folder and delete the file called EPiServer.Templates.Public.dll.

All the code is also available on EPiCode for you to checkout and download.

That’s it! In the next post, we’ll start building the site by implementing our sites foundation.

Introduction – Create an EPiServer site from scratch

This is the first post in a special Christmas blog series. The series will consist of 9 posts (including this), showing you how to create an EPiServer site from scratch!

Since this series is about EPiServer, I’m not going to focus on design, UX, frontend coding etc. I’ll be basing the site on a free template that I found: Jungleland. I tried finding a more christmassy template, but unfortunately I couldn’t find a suitable one.

Jungleland theme

Part 1: Setting up the development environment

The first part will be posted on Friday 4th of December. This post will show you how to setup the latest version of EPiServer, and the necessary tools for developing with it: MS SQL Server, IIS, .NET, Visual Studio. And also other tools that we’ll be using: Firefox, Firebug, TortoiseSVN, UnleashIt, YSlow.
Read the first part.

Part 2: Creating a foundation

In this post we’ll start on the foundation, adding the resources (images, JavaScript, CSS etc), creating the master page, and other common areas.
Read the second part.

Part 3: Creating the Start page

In this post we’ll create the sites start page. I’ll show you how to retrieve data from other pages and how we can improve this by using extension methods.
Read the third part.

Part 4: Creating the Standard page

In this post we’ll implement the standard page type, with functionality for displaying the article, author, date, creating lists, and sharing the article on sites like Twitter and Facebook.
Read the fourth part.

Part 5: Creating the Search page

In this post we’ll implement search functionality and add a quick search field with auto completion functionality.
Read the fifth part.

Part 6: Creating the XForm page

Our site as a contact us page and other forms. In this post we’ll implement XForm functionality for creating forms and sending custom confirmation emails.
Read the sixth part.

Part 7: Creating the Sitemap page

In this post we’ll create our sites Sitemap page. We’ll also refactor some of our code – and I’ll share some tips on the subject.
Read the seventh part.

Part 8: Preparing for launch

In the last part, we’re preparing for launch. We’ll test the site, and make sure everything works, do performance tests with YSlow, deploy to a remote server, and add tools for monitoring our production site.
Read the eighth part.

Hope this helps – and as always, comments are appreciated! :)

Extending EPiServer Categories

The other day I asked a question on twitter

Is there an easy way of getting all the selected categories from a sub-category in EPiServer for a page?

If you use CurrentPage.Categories or (CategoryList)CurrentPage["MyCategoryProperty"], you’ll get all the selected categories in one list. What I needed was to only get those that where children of a certain one.

Having gotten no answer and not finding anything in the SDK, I decided to create this functionality as a couple of extension methods.

using System.Linq;
using EPiServer.Core;
using EPiServer.DataAbstraction;
 
namespace EPiCode.Extensions
{
    public static class CategoryExtensions
    {
        public static CategoryCollection GetActiveSubCategories(this CategoryList allActiveCategoryIds, string parentCategoryName)
        {
            return allActiveCategoryIds.GetActiveSubCategories(Category.Find(parentCategoryName));
        }
 
        public static CategoryCollection GetActiveSubCategories(this CategoryList allActiveCategoryIds, int parentCategoryId)
        {
            return allActiveCategoryIds.GetActiveSubCategories(Category.Find(parentCategoryId));
        }
 
        public static CategoryCollection GetActiveSubCategories(this CategoryList allActiveCategoryIds, Category parentCategory)
        {
            if (allActiveCategoryIds == null || allActiveCategoryIds.Count < 1 || parentCategory == null)
            {
                return null;
            }
 
            CategoryCollection subCategories = Category.Find(parentCategory.ID).Categories;
 
            var activeCategories = new CategoryCollection();
 
            for (int i = 0; i < allActiveCategoryIds.Count; i++)
            {
                Category category = Category.Find(allActiveCategoryIds.ElementAt(i));
 
                foreach (Category subCategory in subCategories)
                {
                    if (subCategory.ID == category.ID)
                    {
                        activeCategories.Add(category);
                    }
                }
            }
 
            return activeCategories;
        }
    }
}
CategoryCollection activeSubCategories = CurrentPage.Category.GetActiveSubCategories("news");

Note that this will not find the grand or grand grand children, only the direct children of a category.

Removing duplicates from a PageDataCollection

Today I had to remove duplicate pages from a PageDataCollection. I checked for a method that would help me with this in the SDK, but couldn’t find one. I then went to the Filters namespace to see if there was a filter for this, but no luck.

I then remembered that System.Linq has a great extension method for collections that implement IEnumerable, called Distinct, that does exactly what I want.

The code is pretty simple.

// using System.Linq;
myPageDataCollection.Distinct();

Simple and elegant, but of course it didn’t work!

Custom comparer

If you take a look at the documentation for the Distinct method you’ll see an overload method that takes IEqualityComparer(T) for comparing.

Below is a custom comparer for PageData, that implements the IEqualityComparer interface. Pretty simple code.

public class PageDataComparer : IEqualityComparer<PageData>
{
	public bool Equals(PageData a, PageData b)
	{
		return a.PageLink.CompareToIgnoreWorkID(b.PageLink);
	}
 
	public int GetHashCode(PageData pageData)
	{
		return pageData.PageLink.GetHashCode();
	}
}

The updated call to the Distinct method now looks like this.

myPageDataCollection.Distinct(new PageDataComparer());

Custom filter

We can do the same with a custom filter.

public class FilterRemoveDuplicates : IPageFilter
{
	public void Filter(PageDataCollection pages)
	{
		var pageReferences = new List<PageReference>();
 
		for (int pageIndex = pages.Count - 1; pageIndex >= 0; pageIndex--)
		{
			PageData pageData = pages[pageIndex];
			if (pageReferences.Contains(pageData.PageLink))
			{
				pages.RemoveAt(pageIndex);
			}
			else
			{
				pageReferences.Add(pageData.PageLink);
			}
		}  
	}
 
	public void Filter(object sender, FilterEventArgs e)
	{
		this.Filter(e.Pages);
	}
 
	public bool ShouldFilter(PageData page)
	{
		throw new NotImplementedException();
	}
}
new Filters.FilterRemoveDuplicates().Filter(myPageDataCollection);

Flash and Flash Video EPiServer Dynamic Content

I’ve extended Allan Thræn’s Insert Flash elements in the Editor as Dynamic Content, to now use swfobject for a more standards-friendly way of embedding Flash. I’ve also added support for videos, by using Flowplayer, which is a popular Flash Video Player.

Flowplayer

Click here to view it

Edit mode

Solution

Update 18.11.2009

I’ve updated the code to now also use fallback content for users that do not have Flash or JavaScript installed/enabled. When using the fallback content with regular Flash files (.swf), whenever someone who doesn’t have Flash or JavaScript installed/enabled it will display.

For Flowplayer it behaves a bit differently, the fallback content will be displayed when the page loads, when someone clicks it, the video will start. So with no fallback content, the video plays automatically when the page loads. With fallback content (could be an image for instance), the user has to click the image (in this case), to start the video. Here’s an example.

The code is almost identical to Allan’s, the only thing I’ve changed is the Render method and I’ve added an extra text field for the id (must be unique for the page).

Since the fallback content contains HTML code I had to update the code for getting and setting the State. Previously I just used Allan’s code, which used | to separate the different values. But now I had to use XML and Serialization. It was a little tricky at first, but thankfully I came across Anders’ post Dynamic content and State attribute.

using System;
using System.Globalization;
using System.Xml.Linq;
using EPiServer;
using EPiServer.Core;
using EPiServer.DynamicContent;
using EPiServer.Editor;
using EPiServer.SpecializedProperties;
 
namespace FlashDynamicContent
{
    public class FlashDynamicContent : IDynamicContent
    {
        protected PropertyDocumentUrl flash;
        protected PropertyNumber width;
        protected PropertyNumber height;
        protected PropertyXhtmlString fallbackContent;
 
        /// <summary>
        /// Setup properties
        /// </summary>
        public FlashDynamicContent()
        {
            flash = new PropertyDocumentUrl { Name = "Flash file " };
            width = new PropertyNumber(300) { Name = "Width" };
            height = new PropertyNumber(300) { Name = "Height" };
 
            fallbackContent = new PropertyXhtmlString
                                  {
                                      Name = "Fallback content",
                                      EditorToolOptions = EditorToolOption.All ^ EditorToolOption.Font ^ EditorToolOption.DynamicContent
                                  };
        }
 
        public System.Web.UI.Control GetControl(PageBase hostPage)
        {
            throw new NotImplementedException();
        }
 
        public PropertyDataCollection Properties
        {
            get
            {
                return new PropertyDataCollection { flash, width, height, fallbackContent };
            }
        }
 
        public string Render(PageBase hostPage)
        {
            if (flash.ToString().EndsWith(".swf", true, CultureInfo.InvariantCulture))
            {
                if (!hostPage.ClientScript.IsClientScriptIncludeRegistered("swfobject"))
                {
                    hostPage.ClientScript.RegisterClientScriptInclude("swfobject", "http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js");
                }
 
                hostPage.ClientScript.RegisterStartupScript(GetType(), "swfobject" + this.GetHashCode(), string.Format("swfobject.embedSWF(\"{0}\", \"flash{3}\", \"{1}\", \"{2}\", \"9.0.0\", false);", flash, width, height, this.GetHashCode()), true);
 
                return string.Format("<div id=\"flash{0}\">{1}</div>", this.GetHashCode(), fallbackContent);
            }
 
            if (!hostPage.ClientScript.IsClientScriptIncludeRegistered("flowplayer"))
            {
                hostPage.ClientScript.RegisterClientScriptInclude("flowplayer", "/Flowplayer/flowplayer-3.1.4.min.js");
            }
 
            hostPage.ClientScript.RegisterStartupScript(GetType(), "flowplayer" + this.GetHashCode(), string.Format("flowplayer(\"flash{0}\", \"/Flowplayer/flowplayer-3.1.5.swf\", \"{1}\");", this.GetHashCode(), flash), true);
 
            return string.Format("<div style=\"width:{0}px;height:{1}px\" id=\"flash{2}\">{3}</div>", width, height, this.GetHashCode(), fallbackContent);
        }
 
        public bool RendersWithControl
        {
            get { return false; }
        }
 
        public string State
        {
            get
            {
                return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(new XElement("flashcontent",
                    new XElement("flash", flash),
                    new XElement("width", width),
                    new XElement("height", height),
                    new XElement("fallbackcontent", new XCData(fallbackContent.ToString()))).ToString(SaveOptions.DisableFormatting)));
            }
            set
            {
                if (value == null)
                {
                    return;
                }
 
                byte[] toDecodeByte = Convert.FromBase64String(value);
 
                var encoder = new System.Text.UTF8Encoding();
                System.Text.Decoder utf8Decode = encoder.GetDecoder();
 
                int charCount = utf8Decode.GetCharCount(toDecodeByte, 0, toDecodeByte.Length);
 
                var decodedChar = new char[charCount];
                utf8Decode.GetChars(toDecodeByte, 0, toDecodeByte.Length, decodedChar, 0);
 
                var flashContent = XElement.Parse(new string(decodedChar));
                flash.ParseToSelf((string)flashContent.Element("flash"));
                width.ParseToSelf((string)flashContent.Element("width"));
                height.ParseToSelf((string)flashContent.Element("height"));
                fallbackContent.ParseToSelf((string)flashContent.Element("fallbackcontent"));
            }
        }
    }
}

In the Render method I check if the file ends with .swf, depending on that I either add the code for swfobject, or for flowplayer.

Update 19.11.2009

Thanks to Martins comment, I’ve removed the id field and replaced it with the HashCode and a prefix of flash (HTML id have to start with a letter in the roman alphabet). One less thing for the editor to worry about :) .

Installation

  1. Download the code
  2. Unzip, and copy the FlashDynamicContent.dll into your sites bin folder, and the Flowplayer folder into your sites root folder
  3. Register it in your web.config file
    <dynamicContent>
    	<controls>
            ...
    		<add description="Insert Flash or Flash Video" name="FlashDynamicContent" type="FlashDynamicContent.FlashDynamicContent, FlashDynamicContent"/>
    	</controls>
    </dynamicContent>

Better breadcrumb trail

I often install the Public Templates when setting up a new EPiServer project. They contain some good code that I reuse in various parts of a new site. One thing that I’ve copied and modified is the code for the breadcrumb (breadcrumb.ascx). By default the markup rendered looks something like this.

<a href="/en/" title="To start page">Start</a> / 
<a href="/en/Events/" title="Events">Events</a> / 
<a href="/en/Events/Conference/" title="Conference">Conference</a>

There are a number of problems with this code. First of the markup is not very semantic. Second the title attribute is redundant, since the same text gets repeated twice, and thus read by screen readers twice. Third the page we’re currently on should not be a link, since we’re already on it. It should also have some help text for screen reader users giving information that “Conference” in fact is the current page.

Better markup

Okay lets fix the semantics first. Lets put the links inside a list and remove the forward slash (/), which is presentational, something that should be handled by CSS.

<ul>
<li><a href="/en/" title="To start page">Start</a></li>
<li><a href="/en/Events/" title="Events">Events</a></li>
<li><a href="/en/Events/Conference/" title="Conference">Conference</a></li>
</ul>

This is better, but we’re not quite there yet. An unordered list is not the correct list type here. The meaning of the list changes when we change the order of the links, we should therefor use an ordered list instead. Lets update the list to be ordered, and lets also get rid of all the title attributes.

<ol>
<li><a href="/en/">Start</a></li>
<li><a href="/en/Events/">Events</a></li>
<li><a href="/en/Events/Conference/">Conference</a></li>
</ol>

That’s better! Now we only have the “Conference” link left. Lets remove the anchor, and add some helper text for our screen reader users.

...
<li>Conference <span>(this page)</span></li>
...

A little styling

Great, now with a little CSS we can transform this to a nice, user friendly breadcrumb trail.

#breadcrumb {
    list-style: none;
}
 
#breadcrumb li {
    display: inline;
    margin: 0;
    padding: 0;
}
 
#breadcrumb a
{
    color: #3e3e3e;
	float: left;
	margin-right: .5em;
	padding-right: 1em;
	background: url(images/separator.gif) no-repeat right center;
}
 
#breadcrumb span {
    position: absolute;
    left: -9999px;
}

Nothing particular here, instead of adding extra markup for the separator, we’re using a background image. And for the helper text we’re using a technique for pushing the span element all the way to the left (off screen), hiding it from all except screen reader users. Why not use display: none instead? Well because some screen readers don’t read text that has the display property set to none.

The result


Nothing exciting here, but we now have a much more semantic breadcrumb, that is very easy to style to our needs.

Implementation in EPiServer

This is code based on the Public Templates breadcrumb.ascx, modified for our purpose.

using System;
using EPiServer.Core;
using System.Text;
 
namespace EPiServer.Templates.Public.Units.Static
{
    public partial class BreadCrumbs : UserControlBase
    {
        private const string _link = "<li><a href=\"{0}\">{1}</a></li>";
        private int _maxLength = 60;
        private string _breadcrumbId = "breadcrumb";
 
        protected override void OnPreRender(EventArgs e)
        {
            Breadcrumbs.Text = GenerateBreadCrumbs(CurrentPage);
        }
 
        private string GenerateBreadCrumbs(PageData page)
        {
            // Initiate a string builder based on max length. The generated html is considerably longer than the visible text.
            var breadCrumbsText = new StringBuilder(8 * MaxLength);
 
            // Initiate a counter holding the visible length of the bread crumbs with the length of the start page link text.
            int breadCrumbsLength = Translate("/navigation/startpage").Length;
 
            while (page != null && !page.PageLink.CompareToIgnoreWorkID(PageReference.StartPage))
            {
                breadCrumbsLength += page.PageName.Length;
                if (breadCrumbsLength > MaxLength)
                {
                    breadCrumbsText.Insert(0, "...");
                    break;
                }
 
                // Insert the link at beginning of the bread crumbs string 
                breadCrumbsText.Insert(0, this.GetLink(page));
 
                // Get next visible parent
                page = this.GetParentPageData(page);
            }
 
            // Generate the start page link 
            string startPageLinkUrl = string.Empty;
            if (string.IsNullOrEmpty(startPageLinkUrl))
            {
                startPageLinkUrl = Server.UrlPathEncode(GetPage(PageReference.StartPage).LinkURL);
            }
 
            string startPageLink = string.Format(_link, startPageLinkUrl, Translate("/navigation/startpage"));
            breadCrumbsText.Insert(0, startPageLink);
 
            string listStart = string.Format("<ol id=\"{0}\"", _breadcrumbId);
 
            breadCrumbsText.Insert(0, listStart);
            breadCrumbsText.Append("</ol>");
            return breadCrumbsText.ToString();
        }
 
        /// <summary>
        /// Get the next visible parent page of the supplied <see cref="PageData"/>. 
        /// </summary>
        /// <param name="page"></param>
        /// <returns>The <see cref="PageData"/> object or    </returns>
        private PageData GetParentPageData(PageData pageData)
        {
            // Don't return a PageData object for start page or root page.
            if (pageData == null || pageData.ParentLink == PageReference.StartPage || pageData.ParentLink == PageReference.RootPage)
            {
                return null;
            }
 
            // Get Page data for parent page
            pageData = GetPage(pageData.ParentLink);
            if (pageData != null && pageData.VisibleInMenu)
            {
                return pageData;
            }
            // Step up to next parent
            return GetParentPageData(pageData);
        }
 
        /// <summary>
        /// Returns a anchor based on a <see cref="PageData"/> object.
        /// </summary>
        private string GetLink(PageData page)
        {
            string pageName = page.Property["PageName"].ToWebString();
 
            if (page.PageLink.CompareToIgnoreWorkID(CurrentPage.PageLink))
            {
                return string.Format("<li>{0} <span>(this page)</span></li>", pageName);
            }
 
            return string.Format(_link, Server.UrlPathEncode(page.LinkURL), pageName);
        }
 
        /// <summary>
        /// Sets the max length on the visible breadcrumb text (default = 60)
        /// </summary>
        public int MaxLength
        {
            get { return _maxLength; }
            set { _maxLength = value; }
        }
 
        public string BreadcrumbId
        {
            get { return _breadcrumbId; }
            set { _breadcrumbId = value; }
        }
    }
}

One thing to note here is that I’ve hard coded the text (this page). This should be placed in a language file instead.

Other resources

The SelectedTemplate and duplicate code

The SelectedTemplate is used in the MenuList and PageList EPiServer web controls. It is a template used for displaying selected items in navigation lists. This is a nice template to have, but sometimes it is an overkill to use it. Say when you only need to add a class of selected or active (which happened to me today).

The problem

<ul class="nav">
	<li>
		<a href="#">
			<span class="l"></span><span class="c">PageName</span><span class="r"></span>
		</a>
	</li>
	<li>
		<a href="#" class="active">
			<span class="l"></span><span class="c">PageName</span><span class="r"></span>
		</a>
	</li>
</ul>

This is a simple navigation list, but as you can see we have two span tags inside the anchors (this is a technique used when you need to use more than one background image). So we cannot use the EPiServer:Property control that we would use normally.

<EPiServer:Property PropertyName="PageLink" runat="server" />

The normal solution

Instead we’re using a combination of HTML and data binding syntax.

<ItemTemplate>
	<li>
		<a href="<%# Container.CurrentPage.LinkURL %>">
			<span class="l"></span><span class="c"><%# Container.CurrentPage.Property["PageName"].ToWebString() %></span><span class="r"></span>
		</a>
	</li>
</ItemTemplate>
<SelectedTemplate>
	<li>
		<a href="<%# Container.CurrentPage.LinkURL %>" class="active">
			<span class="l"></span><span class="c"><%# Container.CurrentPage.Property["PageName"].ToWebString() %></span><span class="r"></span>
		</a>
	</li>
</SelectedTemplate>

This will work fine, but is not good practice, since we now have duplicated the same code in both templates, only adding class=”active” to the SelectedTemplate.

Solution

We’re now going to update the code to only use the ItemTemplate, and add a little logic for checking if the item should be selected or not.

<ItemTemplate>
	<li>
		<a href="<%# Container.CurrentPage.LinkURL %>" <%# SelectedCssClass(Container.CurrentPage, "active") %>>
			<span class="l"></span><span class="c"><%# Container.CurrentPage.Property["PageName"].ToWebString() %></span><span class="r"></span>
		</a>
	</li>
    </ItemTemplate>

The SelectedCssClass method lives in our code-behind file and looks like this:

protected string SelectedCssClass(PageData page, string cssClass)
{
	var result = string.Empty;
	if (page == null || page.PageLink.ID < 1)
	{
		return result;
	}
 
	if (page.IsSelected(CurrentPage))
	{
		result = string.Format("class=\"{0}\"", cssClass);
	}
 
	return result;
}

As you can see I’m using an extension method (from the EPiCode.Extensions project) in my if statement. This method simply takes the CurrentPage and checks if it, or any of its parents, match the page that is being data binded (same thing as the SelectedTemplate does).

By adding this logic to our code-behind (or a helper class), instead of using the SelectedTemplate just for this little thing, we make it much easier for us or (even more important) the next developer to change and update the navigation list. We’ve eliminated a code smell :) .

Specify your preferred external URL in EPiServer

Here’s a little SEO tip – search engines give you a penalty for having duplicate content. Duplicate content can be different urls going to the same content, eg:

  • http://www.example.com/tags/episerver/sort=newest
  • http://www.example.com/tags/episerver/sort=oldest
  • http://www.example.com/tags/episerver/

These are all pointing to the same content. The problem with this is that your PageRank or link popularity will decrease because of this. Fortunately it is very easy to specify a preferred URL that Search Engines will use. You simply add a HTML link element, with the rel attribute set to canonical, and the href to the preferred URL.

<link rel="canonical" href="http://www.example.com/tags/episerver" />

In EPiServer we also have a simple way of setting a simple address to a page. Under Advanced Information and Simple address for this page.
EPiServer edit mode - Advanced Information
Now we only need to add a little code to get the value, generate the HTML link element and add it to the HTML head element. If you’re using code based on the Public Templates, this goes in the Header.ascx and under the CreateMetaData method.

if (IsValue("PageExternalURL"))
{
	var canonicalLink = new HtmlGenericControl("link");
	canonicalLink.Attributes.Add("rel", "canonical");
	canonicalLink.Attributes.Add("href", EPiServer.Configuration.Settings.Instance.SiteUrl + CurrentPage["PageExternalURL"].ToString());
 
	this.plhMetaDataArea.Controls.Add(canonicalLink);
}

The key here is the PageExternalURL property, which is an EPiServer default property (See the SDK for more). You could of course create your own unique property for this.

This will render the following markup:

<link rel="canonical" href="http://www.example.com/MyPage" />

Note that you don’t need to include EPiServer.Configuration.Settings.Instance.SiteUrl (http://www.example.com), only CurrentPage["PageExternalURL"].ToString() (/Mypage) will work fine as well. But I prefer to include the main domain name for the site (from the web.config setting, siteUrl).

For more information see this blog post from Google.

EPiCode.Extensions new EPiCode Community Project

I’m a huge fan of extension methods. In every project I have at least a couple of them. Always adding new ones. If you read other blogs you see that I’m not the only one. We all have a couple of classes with extension methods we use in our projects. Some of these have been shared with the community, through blogs and presentations. But a lot are hidden, either inside a project or in a partners code library.

I don’t know about you, but for me extension methods save me a ton of time, and make it so much more fun to code! That is why I started the EPiCode.Extensions project. So that we all can share what we have, and have even more fun developing EPiServer sites :) .

CodeResort

Update 03.11.2009 – Added EPiCode Extensions wiki page

The project is hosted on EPiCode, and consists of two projects: EPiServer.Extensions and EPiServer.Extensions.Tests.

The convention is fairly simple. EPiServer.Extensions.PageDataExtensions contains the extensions for the PageData class, EPiServer.Extensions.PageReference contains the extensions for the PageReference class and so forth. All the classes are partial.

I’ve started to add some of my extension methods, and the ones that I’ve found useful from various other blogs. I’m hoping that you’ll do the same and add more :) . To checkout the code you have to install a Subversion client like TortoiseSVN. The Subversion URL is: https://www.coderesort.com/svn/epicode/EPiCodeExtensions/.

Happy coding!

SlideShare Dynamic Content

In previous posts I’ve blogged about the SlideShare API. In this post I’m going to show how you can use it to create a Dynamic Content Plugin that lets your editors choose a presentation from SlideShare.net and display it on their web page.

Before we start I recommend that you read through my SlideShare API post and also browse the code to get a better picture of what we can do and what is available for you to extend and build on.

I would like to thank Peter Sunna for his excellent blog post Display youtube videos in episerver using dynamic content – which this code is based on.

The plugin consists of two parts, an admin plugin for storing settings, and the dynamic content plugin for choosing a presentation.

Admin plugin

SlideShare Admin plugin
Take a look at my post Creating an EPiServer Plugin for more information.

To get the API and Shared Secret keys go to the Apply for API Keys page and fill out the form. You should then receive an email with both keys.

Dynamic content plugin

SlideShare Dynamic Content plugin

With paging:
SlideShare Dynamic Content plugin with paging

I’ve only added the ability for editors to choose a presentation from one user. But if you take a look at the SlideShare API, you see all the other methods we can use to improve this even further.

View mode

Installation

  1. Download and extract FV.DynamicSlideSharea
  2. Open up the Install folder
  3. Copy the dll files from the bin folder to your sites bin folder
  4. Copy the language file from the lang folder
  5. Copy the whole FV.DynamicSlideShare folder
  6. Register the dynamic content plugin in your sites episerver.config file
    <dynamicContent>
    	<controls>
    		...
    		<add description="DynamicSlideShare" name="DynamicSlideShare" type="FV.DynamicSlideShare.Plugins.DynamicSlideShare, FV.DynamicSlideShare"/>
    	</controls>
    </dynamicContent>

Remember to fill out the settings in admin mode, under Tools and SlideShare Admin.

Apply for API and Security key.

Download FV.DynamicSlideShare from EPiCode, or SVN Checkout the source code.

© Copyright Frederik Vig. Based on Fluid Blue theme