EPiServer  /  CMS January 17, 2011

Sweet EPiServer Templating with TypedPageList

Recently I have been working on a custom web control with similar capabilities as EPiServers PageList control. This control, as opposed to the PageList control, handles statically typed subclasses of PageData. It also exposes a number of extension points that I hope should make it flexible and nice to work with.

Background

While I spent the better part of the previous year trying to work with EPiServer CMS in a highly testable and “non-Web Formsy” way while still using Web Forms, such as with EPiMVP, I’m currently involved in a project where that really isn’t an option. Instead we’re trying to embrace the good parts of Web Forms in order to limit usage of the bad parts.

In this context there has been some discussion regarding the web controls that ship with EPiServer, such as the PageList control. While those controls enable features to be developed very rapidly I have two problems with them. Naturally they don’t have support for the typed PageData objects you get with Page Type Builder. Another problem is that often when I’ve used them I’ve found that there was some little detail that I just couldn’t do. Or that I had a hard time doing. In those cases I had trouble finding workarounds as they lack some extensibility points (that might just be being blind though).

Therefor I’ve begun working on a custom web control of my own named TypedPageList. Unfortunately there isn’t much use for a web control with generic type parameters as you can’t use it in markup (in aspx and ascx files etc) without workarounds that involve losing some of the static typing. Therefore, to use TypedPageList we need to subclass it and specify what types of pages each subclass will be used for. Given the benefits though, I think that that’s a price worth paying.

Download and source code

You can grab the source code from the repository on GitHub where you’ll also find ready-to-use binaries.

Disclaimer

I have limited experience with custom web control development, especially as of late. That means that I might very well have missed something of importance. I should also mention that the control, or at least my code, doesn’t utilize view state, unlike the controls that ship with EPiServer. This means that the control has to be data bound after post backs and it might cause problems in scenarios where the control is used to edit PageData objects or such. That is often easy to handle though and in return your visitors won’t have to load a lot of view state data.

Further, the code will probably be subject to change in the future. Especially the paging, which is described later on in the post, might have to be handled in a different way to accommodate scenarios where it should be done in the database.

Using TypedPageList

Naturally the first step in using the control is to download it and add a reference to it in our web project, or some other project that our web project will use. With that done we’re ready to create an implementation.

Creating a type aware Page List

To create an implementation of TypedPageList we create a new class in that inherits from it an which specifies what type it should handle. In the below example we assume that there is a page type class called Article and we create a control that will list instances of that type. Of course it will also be able to handle subclasses of Article.

using System.Web.UI;
using MySite.PageTypes;
using TypedTemplating;

namespace MySite.Controls
{
    public class ArticleList : TypedPageList<Article>
    {
    }
}

This is the bare minimum that we have to do. However, in order to make the different item templates type aware we need to override them and specify the type. The control supports a number of item templates, which we’ll soon look at, but for now, let’s begin by overriding the most general, ItemTemplate.

using System.Web.UI;
using MySite.PageTypes;
using TypedTemplating;

namespace MySite.Controls
{
    public class ArticleList : TypedPageList<Article>
    {
        [TemplateContainer(typeof(PageListPageItem<Article>))]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public override ITemplate ItemTemplate { get; set; }
    }
}

With that done we can proceed to add the control to a page. The first step in doing that is to register the control with a tag prefix at the top of the page.

<%@ Register Namespace="MySite.Controls" 
    Assembly="MySite.Web" TagPrefix="TT" %>
Alternatively we can register it on a site wide level by adding the below line to the pages/controls node in web.config.
<add tagPrefix="TT" 
    namespace="MySite.Controls" 
    assembly="MySite.Web" />

With the control’s namespace registered we can add it to the page and specify an ItemTemplate.

<TT:ArticleList ID="articleList" runat="server">
    <ItemTemplate>
    <p>
        <%# Container.DataItem.PageName %>
    </p>
    </ItemTemplate>
</TT:ArticleList>

If you’re used to working with EPiServer’s web controls you have probably noticed that we’re using Container.DataItem instead of Container.CurrentPage in the data binding expression in the item template. The CurrentPage property is there as well but that is of type PageData while the DataItem property is of type Article. The reason for this is that the container object’s class implements the IPageSource interface and I didn’t want the class to have two properties named CurrentPage although that would have been technically possible.

Anyway, since the container’s class implements IPageSource we can rewrite the above markup to use the Property control instead should we want to.

<TT:ArticleList ID="articleList" runat="server">
    <ItemTemplate>
    <p>
        <EPiServer:Property PropertyName="PageName"
            runat="server" />
    </p>
    </ItemTemplate>
</TT:ArticleList>

If we now view the page won’t see any output. That’s because the control hasn’t been data bound, and if it had it wouldn’t know what pages to list. There are a few ways to fix this, both from markup and code behind as well as directly in the control class it self.

To illustrate basic usage we can set a root node to list pages from and tell the control to automatically do data binding in markup.

<TT:ArticleList ListingRoot="<%# CurrentPage.PageLink %>" 
    AutoBind="true" ID="articleList" runat="server">
    <ItemTemplate>
    <p>
        <%# Container.DataItem.PageName %>
    </p>
    </ItemTemplate>
</TT:ArticleList>

The same effect can be achieved from code behind, like this:

articleList.ListingRoot = CurrentPage.PageLink;
articleList.DataBind();
The control will now list all children of the current page that are of type Article.

Different ways of fetching pages

The control supports two ways of listing pages. If no listing root has been specified it will check if it’s DataSource property has been set and if so, list those pages. In the case of our ArticleList control the DataSource property will be of type IEnumerable<Article>.

While it’s possible to specify an enumeration of pages to list the primary intended usage is to set the ListingRoot property. By default the control will then list the children of that page. However, it is possible to change this behavior by setting the controls ListingStrategy property to an implementation of the IListingStrategy<TPageData> (in our case TPageData is Article). This enables us to provide whatever means of fetching pages that we want.

While we can create whatever listing strategy we want the project ships with two that we can use out-of-the-box, Children and RecursiveChildren that both are located in the TypedTemplating.Listing namespace.

The Children strategy simple retrieves the children of the listing root that have correct page type(s).

page structure in EPiServers page treeThe RecursiveChildren strategy is a bit more advanced. It’s constructor requires a parameter with which we specify how many levels below the root page that we want to list. For instance, let’s assume that we have the page structure displayed in the image to the right. The pages below the Articles page are either of type Article or Review. The latter is a subtype of Article. If we view the page named Articles (whose template hosts the control) and configure the control with the code below the we will see four pages, Review 1, Article 2, Article 1 and Review 2. 

articleList.ListingRoot = CurrentPage.PageLink;
articleList.ListingStrategy =
    new RecursiveChildren<Article>(2);
articleList.DataBind();

Templating

So far we’ve only used the ItemTemplate template. However, the TypedPageList control supports three other templates for items. These are AlternatingItemTemplate, FirstItemTemplate and LastItemTemplate. Just as for the ItemTemplate we need to override their properties in our implementation. A complete implementation of our ArticleList control with all templates overridden looks like this:

public class ArticleList : TypedPageList<Article>
{
    [TemplateContainer(typeof(PageListPageItem<Article>))]
    [PersistenceMode(PersistenceMode.InnerProperty)]
    public override ITemplate ItemTemplate { get; set; }

    [TemplateContainer(typeof(PageListPageItem<Article>))]
    [PersistenceMode(PersistenceMode.InnerProperty)]
    public override ITemplate AlternatingItemTemplate { get; set; }

    [TemplateContainer(typeof(PageListPageItem<Article>))]
    [PersistenceMode(PersistenceMode.InnerProperty)]
    public override ITemplate FirstItemTemplate { get; set; }

    [TemplateContainer(typeof(PageListPageItem<Article>))]
    [PersistenceMode(PersistenceMode.InnerProperty)]
    public override ITemplate LastItemTemplate { get; set; }
}

If an item matches several of the templates the most specific will be used. The FirstItemTemplate will be used if it has been specified and there is a single item in the list.

Templates for item header and footer

In many cases providing templates for the different kind of items (alternating, first etc) can be a bit of a pain. Especially since we often just want to add an extra CSS class or similar. Therefore TypedPageList provides separate templates that will be used before or after each type of item. These are as follows:

  • ItemHeaderTemplate
  • ItemFooterTemplate
  • AlternatingItemHeaderTemplate
  • AlternatingItemFooterTemplate
  • FirstItemHeaderTemplate
  • FirstItemFooterTemplate
  • LastItemHeaderTemplate
  • LastItemFooterTemplate

The item header and footer templates aren’t statically typed so we don’t have to override their properties. However, should we want statically typed access to the page being listed that’s possible simply by overriding them just as for the item templates.

These templates are rendered based on the type of item being rendered without regards to what item template is used. Here’s an example of their usage:

<TT:ArticleList ID="articleList" runat="server">
    <ItemTemplate>
        <%# Container.DataItem.PageName %>
    </ItemTemplate>
    <ItemHeaderTemplate>
        <div>
    </ItemHeaderTemplate>
    <ItemFooterTemplate>
        </div>
    </ItemFooterTemplate>
    <LastItemHeaderTemplate>
        <div class="last">
    </LastItemHeaderTemplate>
</TT:ArticleList>

In the above example each item will be wrapped in a div element. The last item’s div element will have the CSS class last.

The same rules used when deciding what item template to use applied for the item header and footer templates.

Other templates

Besides the templates described above the control supports one more template, the SeparatorTemplate which, if specified, will be inserted between items in the list.

So far I haven’t added the header and footer templates found in many other web controls. Adding them wouldn’t be hard but I’ve never seen much use for them.

Access filtering

The DataSource or the listing strategy is only the first step in deciding what pages to list. With a set of pages retrieved from either of those two sources the collection undergoes a number of filtrations before being listed. The first is filtering for access rights and publication status.

We can decide how the access filtering should be done by setting the controls AccessFilteringStrategy property to an implementation of the IAccessFilteringStrategy<TPageData> interface. The project ships with a single implementation called FilterForVisitor which is also used by default. The FilterForVisitor class uses the EPiServer class with the same name to filter the pages. In other words, the control will by default only list published pages that the visitor has sufficient access rights to see.

Filtering

Once access filtering has been performed the pages are put through another filtration step in which we can perform other types of filtering. By default the control doesn’t do anything during this step as it’s FilteringStrategy property has been set to an instance of the NoFiltering class that ships with the project. However, just as for access filtering we can change this by setting the property to an instance of another class. In this case the class must implement the IFilteringStrategy<TPageData> interface.

Out-of-the-box we can use two implementations for filtering that ships with the project, except for the already mentioned NoFiltering class. These are VisibleInMenus<TPageData> and BranchBelowRootVisibleInMenus<TPageData>. The former will filter out any page whose VisibleInMenu property is false. The latter will filter out pages who either have their VisibleInMenu property set to false and pages whose parents, grandparents etc shouldn’t be visible in menus. It will however only check ancestors that are below the listing root.

Below is an example using the BranchBelowRootVisibleInMenus strategy.

articleList.ListingRoot = CurrentPage.PageLink;
articleList.FilteringStrategy =
    new BranchBelowRootVisibleInMenus<Article>(
        CurrentPage.PageLink);
articleList.DataBind();

page-structure-in-EPiServers-page-tree-with-page-not-visible-in-menusIf we were to set the Review 1 page to hidden in menus in the previous example, as in the image to the right, we would only see Article 1 and Review 2 in the list. Review 1 wouldn’t be listed as it’s not visible in menus and Article 2 wouldn’t be listed as it has an ancestor (Review 1) in the tree that is located below the listing root which isn’t visible in menus.

Paging

The final step before rendering the list of pages is to do paging. Unlike EPiServer’s PageList control TypedPageList doesn’t have full paging functionality built in. It does however have support for rendering a subset of the pages that it should list, making it very easy to implement custom paging functionality in whatever way we want.

By default however, no paging is done. We can change that by setting the control’s PagingStrategy property to an implementation of the IPagingStrategy interface. When implementing that interface we must provide a method named GetPagingSpecification which receives the total number of items as a parameter and must return an instance of the PagingSpecification class. That class in turn has two properties, PageSize and PageNumber.

With all other filtering done the control will fetch a PagingSpecification from the PagingStrategy property and ensure that the correct subset of pages according to it is listed. Besides the NoPaging class which returns a PagingSpecification with PageNumber set to 1 and PageSize set to int.MaxValue-1 the project ships with another ready-to-use implementation called QueryStringPaging.

QueryStringPaging will determine the PageNumber depending on the query string. By default the query string key is “q” but that can be changed. The PageSize will default to 10 but that can also be changed.

Below is a rudimentary implementation of paging functionality using QueryStringPaging. This is only to illustrate how the TypedPageList control should be used, not how to build good paging functionality. Don’t copy this code :)

protected const int PagingPageSize = 3;
protected string pagingKey;

protected void Page_Load(object sender, EventArgs e)
{
    var paging = new QueryStringPaging
        {
            PageSize = PagingPageSize
        };
    pagingKey = paging.QueryStringKey;

    articleList.ListingRoot = CurrentPage.PageLink;
    articleList.PagingStrategy = paging;
    articleList.DataBind();

    var pageNumbers = new List<int>();
    for (int pageNumber = 1;
        (pageNumber - 1) * PagingPageSize 
            < articleList.TotalNumberOfPages;
        pageNumber++)
    {
        pageNumbers.Add(pageNumber);
    }
    rptPaging.DataSource = pageNumbers;
    rptPaging.DataBind();
}

protected string GetPagingLink(int page)
{
    return UriSupport.AddQueryString(
        CurrentPage.LinkURL,
        pagingKey,
        page.ToString());

}

The code above begins by declaring a constant, the page size for paging, and a field with the query string key used for paging which will be set in the Page_Load method.

The Page_Load method creates a new QueryStringPaging object and sets it’s page size. It then sets the pagingKey field to the key used by default by the QueryStringPaging object. We could just as well have done the opposite, specified a value for the key and told the QueryStringPaging object to use that.

The Page_Load method goes on to initialize the ArticleList control by setting it’s LinkRoot property, telling it to use the newly created QueryStringPaging object for paging and then data binding it.

Finally it creates a list of integers, one for each paging page, and hooks up a repeater with that as data source. Of course in a real scenario this method should be split up into three self documenting methods :)

We also create a protected method that builds up a URL to the current page with a query string parameter using the pagingKey field and a page number that it receives as a parameter.

With this in the code behind for our page all we have to do is add the actual repeater in the markup and we have very basic but functional paging functionality for our list.

<asp:Repeater runat="server" ID="rptPaging">
    <ItemTemplate>
        <a href="<%# GetPagingLink(
                     (int)Container.DataItem) %>">
            <%# Container.DataItem %>
        </a>
    </ItemTemplate>
</asp:Repeater>

An alternative way to do paging

TypedPageList offers another way to do paging except specifying a paging strategy. It also exposes two properties, one named PagingPage and the other named PagingPageSize. If we set one of these that value will be used instead of the value supplied by the paging strategy. Note that if we only set one of them the other will still use the value from the strategy.

Container properties

Item templates, as well as item header and footer templates, are instantiated in an instance of the generic PageListPageItem class. Besides providing statically typed access to the PageData object through the DataItem property the class also provides a few other properties that may be useful.

ItemIndex – The container’s position in the list. Includes both items with pages and separators.

DataItemIndex – The page in the container’s position in the list, excluding items that don’t contain pages (currently only separators).

page-structure-in-EPiServers-page-treePageLevelBelowRoot – Number of  page’s between the page in the container and the listing root plus one. In the example page tree that we’ve previously looked at the value for Review 1 and Article 1 would be one and the value for Article 2 and Review 2 would be two.

IsCurrentlyViewedPage – Returns true if the page in the container is the currently viewed page.

IsAncestorOfCurrentlyViewedPage – Returns true if the page in the container is the parent, grand parent etc of the currently viewed page.

IsDescendantOfCurrentlyViewedPage – Returns true if the currently viewed page is the parent, grand parent etc of the page in the container.

ItemHeader – A control in which item header templates, such as ItemHeaderTemplate, is instantiated. Might be useful if we want to modify the header in an event handler.

ItemFooter – Same as above but for item footers.

Events and extension points

The control currently exposes two events that we can subscribe to, ItemDataBound and PageItemDataBound. ItemDataBound is triggered for all items, including separators. PageItemDataBound has a generic type parameter and is only triggered for items that contain pages. The latter offers two benefits. First of all we don’t have to check if the bound item is a data item, avoiding bugs that you sometimes run into when using a repeater and you take for granted that all items are data items and then add a header template or a separator template. Second, it offers statically typed access to the page in the item as well as all other properties in the container which we looked at under the previous heading.

Except for specifying strategies for getting pages, filtering etc and the events we can also customize the control by overriding one of it’s method or properties. Pretty much all public or protected members are virtual and designed and the base class should be able to handle that you override them as long as you yourself know what you are doing.

For instance you could easily add a new type of item header template by creating a new property in your control class and override the GetItemTemplateHeader to add logic for determining when it should be used.

Usage scenarios

Admittedly using TypedPageList demands a little more work up front than using for instance EPiServer’s PageList control as you have to create your own implementations where you specify the type and register those. With that done however I think the benefits may outweigh the cost even if you only use the control in a single place. I’m not sure about that myself yet though as I haven’t used it in a real scenario yet.

When this solution really should excel though is when we need to use the same control in a number of places. And if we have some sort of base library with a standard set of page types that we always use we can ship that with a corresponding set of statically typed and highly flexible controls for listing instances of those page types. We could even ship along a number of controls for each page type, preconfigured for different usage scenarios, making development of listings a breeze.

Future development

I’d like to try the control out for a while before making drastic changes but one thing that I’d like to add is support for different templates for pages on different levels in a page hierarchy. Most of the logic is already there so it should be fairly easy.

Any other ideas are most welcome!

Feedback

Don’t be shy, give it to me! But please be kind :)

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