Frederik Vig – ASP.NET developer

Follow me

Archive for November 2009

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.

So what do you think, is this useful? Should I had it to the EPiCode.Extensions project?

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!

© Copyright Frederik Vig. Based on Fluid Blue theme