EPiServer  /  CMS April 11, 2011

Automatically organize EPiServer pages

Need to group pages in EPiServer CMS by date, first letter in their names, or some other criteria? Here's an elegant way of doing that.

Last week Ted Nyberg, Rouslan Minasian, Marthin Freij and me were discussing different solutions for automatically organizing EPiServer CMS pages in various structures. The most common of which is perhaps the classical news archive where pages are grouped by date, like this:

DateStructure

Of course we don’t want to shift the burden of creating such structures over to the editor so some automatic mechanism is required. Although this has certainly been done before it’s often done in such a way that it only works for a specific node which the site code is aware about. We wanted to find a more generic way that could handle an unlimited amount of nodes used to automatically group pages as well as a way to handle many different types of structures.

We came up with a rough idea on how to solve this that we all thought was pretty elegant. We would create an interface that page type classes (using Page Type Builder) could implement. We would then listen to relevant events from DataFactory. If a page was about to be put as a child of a page whose page type implemented this interface we would ask the parent page for a new parent for the page being created or moved.

AlphabeticalStructureOf course I couldn’t help myself and had to prototype this over the weekend :-). The result was a module that makes it easy to automatically group pages by date, by the first letter in their names and by their page types. It also makes it easy to implement many other types of groupings, as well as mixing different types of groupings.

One example of these groupings is the alphabetical, illustrated by the image to the right here. Another is a composite grouping where pages are first group by type and then by date, illustrated in the screenshot below.

TypeAndDateStructure

In this post I’ll describe the implementation of the solution. If you’re more interested in how to use it rather than how it works I suggest jumping directly to the second part.

Disclaimer

I’m usually very skeptical to all functionality that involves automatically creating pages. As soon as a page is created programmatically instead of by an editor the code has a tendency to grow out of control in terms of complexity and debugging becomes increasingly hard. The functionality described here is no exception. However I see no other good way of accomplishing the end goal in this case, so to me this is OK, but it should definitely be used with caution.

Page Structure Builder

While only a prototype I decided to implement it in such a way that it could easily be packaged as a separate module. Therefore I created a new project called Page Structure Builder. You can grab or fork the source code on GitHub.

The project consists of an InitializableModule, the aforementioned interface and a helper for building page structures. It also contains a number of abstract page types which when implemented group their children.

The IOrganizeChildren interface

Conceptually the most important part of my solution is the IOrganizeChildren interface. It’s also the most simple part.

public interface IOrganizeChildren
{
    PageReference GetParentForPage(PageData page);
}

As you can see, a class that implements the interface must implement a single method that should return where it thinks a specific page should be saved. While it's possible to return whatever PageReference that one can think of here the idea is that this method should be implemented to locate a suitable parent for the page amongst it’s existing descendants. If no suitable descendant exists it should create it and return it. We’ll see an implementation of this soon.

The module

In theory the solution for organizing pages in various structures is very, very simple. All we have to do is intercept whenever a page is about to be created or moved by listening to events from DataFactory. If a page is about to be put as a child of a page whose page type implements IOrganizeChildren we will just ask the parent for a (possibly) new location for the page and make sure it’s put there. Sounds like 20 lines of code right?

Unfortunately it turned out that I had to treat the two scenarios of a new page being created and an existing page being moved differently. To make the solution more powerful I also decided to handle the scenario where the returned new parent also implements IOrganizeChildren. So, I ended up with about 80 lines of code instead. Anyhow, all of this logic resides in the initialization module, PageStructureBuilderModule. Let’s go through it step by step.

First of all we have the class it self. It implements EPiServer’s IInitializableModule interface and is annotated with a ModuleDependency attribute to ensure that it’s started after Page Type Builder has been initialized.

[ModuleDependency(typeof(PageTypeBuilder.Initializer))]
public class PageStructureBuilderModule : IInitializableModule
{
    public void Preload(string[] parameters)
    {
        throw new NotImplementedException();
    }
}

When implementing IInitializableModule three methods have to be implemented, Initialize, Uninitialize and Preload. As you can see above the Preload method simply throws a NotImplementedException as prescribed by EPiServer.

The Initialize and Uninitialize methods are slightly more interesting. In them we add and remove listeners to DataFactory’s CreatingPage and MovedPage event.

public void Initialize(InitializationEngine context)
{
    DataFactory.Instance.CreatingPage += DataFactoryCreatingPage;
    DataFactory.Instance.MovedPage += DataFactoryMovedPage;
}

public void Uninitialize(InitializationEngine context)
{
    DataFactory.Instance.CreatingPage -= DataFactoryCreatingPage;
    DataFactory.Instance.MovingPage -= DataFactoryMovedPage;
}

The two methods that we attach to these events, DataFactoryCreatingPage and DataFactoryMovedPage both serve the same purpose but differ in implementation. Let’s begin by looking at the first.

void DataFactoryCreatingPage(object sender, PageEventArgs e)
{
    var parentLink = e.Page.ParentLink;
    var page = e.Page;
    parentLink = GetNewParent(parentLink, page);

    e.Page.ParentLink = parentLink;
}

As you can see it extracts the page that is being created and the reference to it’s parent from the event arguments. It then passes these to another method named GetNewParent, which we’ll soon look at, to figure out which parent the page that is being created should have. Finally it changes the page’s parent to the new parent.

I would have wanted to have the exact same implementation for the scenario when a page is being moved. Unfortunately though I wasn’t able to accomplish that as the event arguments aren’t populated with the page that is being moved in that scenario, and worse, I couldn’t find a way to assign a different location to which the page should be moved. Therefore the DataFactoryMovedPage method was necessary.

void DataFactoryMovedPage(object sender, PageEventArgs e)
{
    var parentLink = e.TargetLink;
    var page = DataFactory.Instance.GetPage(e.PageLink);
    parentLink = GetNewParent(parentLink, page);

    if (PageReference.IsValue(parentLink) 
        && !e.TargetLink.CompareToIgnoreWorkID(parentLink))
    {
        DataFactory.Instance.Move(page.PageLink, parentLink, 
            AccessLevel.NoAccess, AccessLevel.NoAccess);
    }
}

The first three lines are similar to the same lines in the DataFactoryCreatingPage method but it retrieves the page and the parent in a different way. It then proceeds to check if the page should have a different parent than the one it was originally being moved to. If it should, it moves the page to the new parent. This means that the page might be moved twice which of course isn’t optimal. In practice though I don’t think it matters much.

As we saw above both the methods for handling DataFactory events called a method named GetNewParent which returned a new parent for the page that was being created or moved. This, including a couple of helper methods which it uses, is the final step in our walkthrough of the module.

private PageReference GetNewParent(
    PageReference originalParentLink, PageData page)
{
    var queriedParents = new List<PageReference>();

    var organizingParent = GetChildrenOrganizer(originalParentLink);

    PageReference parentLink = originalParentLink;
    while (organizingParent != null 
        && ListContains(queriedParents, parentLink))
    {
        queriedParents.Add(parentLink);
        var newParentLink = organizingParent.GetParentForPage(page);
        if (PageReference.IsValue(newParentLink))
        {
            parentLink = newParentLink;
        }
        organizingParent = GetChildrenOrganizer(parentLink);
    }
    return parentLink;
}

private bool ListContains(
    List<PageReference> queriedParents, PageReference parentLink)
{
    return queriedParents.Count(p => p.CompareToIgnoreWorkID(parentLink)) == 0;
}

private IOrganizeChildren GetChildrenOrganizer(PageReference pageLink)
{
    if (PageReference.IsNullOrEmpty(pageLink))
    {
        return null;
    }

    return DataFactory.Instance.GetPage(pageLink) as IOrganizeChildren;
}

I need to tidy this up a bit but as you can see this is where the interesting logic in the module resides. The GetNewParent method checks if the parent implements IOrganizeChildren. If it does it asks the parent for a new parent for the page. If that new parent also implements IOrganizeChildren it proceeds to ask that too for a new parent for the page. This continues until it encounters a new parent that doesn’t implement IOrganizeChildren, or the returned new parent has already been asked, such as when the parent returns it self.

The StructureHelper class

While the IOrganizeChildren interface and the PageStructureBuilderModule is enough to implement the basic concept of our idea I also wanted to provide a number of ready-to-use page types that would organize their children in various ways. When implementing these I quickly noticed a pattern. In all of the implementations I had to check if the page (the instance of the class that implements IOrganizeChildren) already had a child of a specific type with a specific name. If it didn’t I had to create one.

Therefore I extracted this functionality into a common helper class named StructureHelper. It currently has a single public method, GetOrCreateChildPage. This requires two parameters, the PageReference of the parent and the name of the child. It also has a type parameter which is used to specify the type of the child page. Supplied with these parameters the method does just what the name implies, it returns a child of the parent with the specified name and type by first trying to locate an existing child and, if that’s unsuccessful, creating a new child. I won’t go into more detail than that here, but it’s implemented like this:

public virtual TResult GetOrCreateChildPage<TResult>(
    PageReference parentLink, string pageName)
    where TResult : PageData
{
    var child = GetExistingChild<TResult>(parentLink, pageName);
    if (child != null)
    {
        return child;
    }

    child = CreateChild<TResult>(parentLink, pageName);
    return child;
}

private TResult GetExistingChild<TResult>(
    PageReference parentLink, string pageName)
    where TResult : PageData
{
    var children = DataFactory.Instance.GetChildren(parentLink);
    return children
        .OfType<TResult>()
        .FirstOrDefault(c => c.PageName.Equals(
            pageName, StringComparison.InvariantCulture));
}

private TResult CreateChild<TResult>(
    PageReference parentLink, string pageName)
    where TResult : PageData
{
    TResult child;
    var resultPageTypeId = PageTypeResolver.Instance
        .GetPageTypeID(typeof(TResult));
    child = DataFactory.Instance.GetDefaultPageData(
        parentLink, resultPageTypeId.Value) as TResult;
    child.PageName = pageName;
    DataFactory.Instance.Save(
        child, SaveAction.Publish, AccessLevel.NoAccess);
    return child;
}

Creating structures

This post focused on the background for creating this prototype and how I implemented it. In the next part we’ll look at how to actually use it to organize pages in different types of structures as well as how the implementations of the IOrganizeChildren interface that I’ve already created work.

PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.

Joel Abrahamsson

Joel Abrahamsson

I'm a passionate web developer and systems architect living in Stockholm, Sweden. I work as CTO for a large media site and enjoy developing with all technologies, especially .NET, Node.js, and ElasticSearch. Read more

Comments

comments powered by Disqus

My book

Want a structured way to learn EPiServer 7 development? Check out my book on Leanpub!

More about EPiServer CMS