EPiServer  /  CMS July 29, 2009

A first stab at EPiServer CMS with ASP.NET MVC and Page Type Builder

Lately I’ve been playing around, trying to get EPiServer and ASP.NET MVC to work together, using strongly typed pages delivered by Page Type Builder as models. It was pretty early clear that the big initial challenge would be URLs.

First, a disclaimer. I’m very new to ASP.NET MVC so it’s entirely possible that I’ve misinterpreted a lot of things. Therefore this post shouldn’t be regarded as a tutorial, merely a description of how I currently look at the problems when using MVC with EPiServer and my first attempts at finding solutions for them.

Two different patterns for URLs

The standard URLs in EPiServer is something like /Page.aspx?id=1&epslanguage=en. The Page.aspx part is of course which Web Forms page that should be used to render the information in the PageData objects which ID is specified with the ID query string parameter. Then the Friendly URL Rewriter comes in and rewrites the URL to something like /Page/ChildPage/GrandChildPage/ when the URL is delivered externally, such as in a link. When the same URL is requested the Friendly URL rewriter intervenes and rewrites the URL back to a URL that reflects the actual file structure on the server. It does this by traversing the page tree downwards from the start page, locating a page which PageURLSegment property matches the last segment of the external URL and whose ancestors PageURLSegments matches the other segments of the URL. For a content heavy site where information structure is important this is great.

ASP.NET MVC’s standard way of working with URLs on the other hand is not so content structure oriented but reflects the state of the site as an application. The standard pattern for URLs in MVC is /Controller/Action/Identifier where identifier is something, usually a integer ID or a slug (a URL-segment), that identifies the model object that is being presented, edited or listed.

So, using MVC with EPiServer pretty much the first thing we have to ask ourselves is what pattern we want to use for URLs. Do we want our URLs to reflect content structure or application state?

I’ve been trying to get both URL-patterns to work and while it should definitely be possible, and for many EPiServer sites probably preferably, to use EPiServers content oriented structure it has proven to be pretty tricky. One reason is that using this pattern we have to map both from internal (/Page.aspx?id=1..) to external (/page/child) and then (not necessarily using rewriting though) from external to our real internal MVC URLs (/Controller/Action/ID).

Tired of banging my head against the wall trying to get that to work I decided to try the other pattern, that is using MVC URLs. That is after all probably the one that I will want to use for my new blog anyway. Luckily I was a bit more successful in my attempts with this pattern, much because in this scenario we only have to rewrite internal URLs to external URLs matching the MVC pattern and can leave incoming external URLs alone.

Here’s what I did. Please keep in mind that this is just a first experimentation :)

Setting up the project

I began by setting up a new site with the public templates using Deployment Center. I then proceeded to convert the project to a MVC project by following step two in EPiServers guide for creating a gadget. Finally I added a reference to Page Type Builder and removed the public templates.

Creating the page types

The next step was to create a few page types. I also created a base class that they all inherit from called BasePageData. This, as you will soon see, plays a key role in my solution.

using PageTypeBuilder;

namespace MvcTest.Web.Models
{
    public abstract class BasePageData : TypedPageData
    {
        public abstract string ControllerName { get; }

        public string PageURLSegment
        {
            get
            {
                return this.GetPropertyValue(page => page.PageURLSegment);
            } 
        }
    }
}

The base class has two properties, one abstract that subclasses will override exposing the default controller that should be used for pages of that type. Unfortunately we’ve just made our model into a controller, but I don’t think we have much choice if we are to use EPiServer and I also think the degree of the infraction is fairly low.

The second property just exposes the PageURLSegment property in a strongly typed way.

An example page type looks like this:

using PageTypeBuilder;

namespace MvcTest.Web.Models
{
    [PageType]
    public class Article : BasePageData
    {
        [PageTypeProperty]
        public virtual string MainBody { get; set; }

        public override string ControllerName
        {
            get { return "Article"; }
        }
    }
}

Setting up routes

Next I added some routes in global.asax.

protected void Application_Start(Object sender, EventArgs e)
{
    RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.IgnoreRoute("authui/{*pathInfo}");
    routes.IgnoreRoute("util/{*pathInfo}");
    routes.MapRoute(
        "Default",                                              
        "{controller}/{slug}/{action}",                           
        new { controller = "Home", action = "Index", slug = "" }  
        );

}

The first three tells the routing system to stay away from resource files and EPiServers edit and admin interface. The fourth route is the primary “MVC route”. As you can see I changed it from /Controller/Action/ID to /Controller/ID/Action/. I did this because the URL rewriter has no clue about actions and therefore it’s easiest to omit the action part when rewriting URLs.

Rewriting internal URLs

With the routes set up it was time to make sure that links to pages that are fetched from the LinkURL property of PageData objects are rewritten to match the default route. While in theory this isn’t necessary, we could just work with PageData objects as model objects and never use their LinkURL properties, that would force us to build our own system for creating URLs and EPiServers edit mode would be rendered useless.

I began by changing the default URL rewriter provider to EPiServerIdentityUrlRewriteProvider in web.config.

<urlRewrite defaultProvider="EPiServerIdentityUrlRewriteProvider">

The Identity URL Rewrite Provider doesn’t actually do any rewriting but fires all of the events that the Friendly URL Rewrite Provider does.

My event listener looks like this:

void UrlRewriteProvider_ConvertingToExternal(object sender, UrlRewriteEventArgs e)
{
    string queryID = e.Url.QueryCollection["id"];
    if (queryID == null)
        return;

    int pageID;
    if (queryID.Contains("_"))
    {
        RewriteToWorkPageUrl(e, queryID);
    }
    else if (int.TryParse(queryID, out pageID))
    {
        RewriteToPageUrl(e, pageID);
    }
}

It begins by retrieving the ID parameters value from the querystring. It then proceeds to check if the value contains a underscore character which is used to separate page ID and work page ID when viewing pages in edit mode. If it does contain a underscore it tries to rewrite the URL to something like /Controller/PageID_WorkPageID/. If the querystring parameter value does not contain a underscore the event listener tries to rewrite the URL to something like /Controller/PageURLSegment/.

The RewriteToWorkPageUrl is implemented like this:

private void RewriteToWorkPageUrl(UrlRewriteEventArgs e, string queryID)
{
    int pageID;
    string[] splitQueryID = queryID.Split(
        new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);

    if (int.TryParse(splitQueryID[0], out pageID))
    {
        BasePageData page = (BasePageData)DataFactory.Instance.GetPage(new PageReference(pageID));
        string path = "/" + page.ControllerName + "/" + queryID + "/";
        RewiteUrl(e, path);
    }
}

private void RewiteUrl(UrlRewriteEventArgs e, string path)
{
    e.Url.Path = path;
    e.Url.QueryCollection.Remove("id");
    e.Url.QueryCollection.Remove("epslanguage");
    e.IsMappableUrl = true;
    e.IsModified = true;
}

And the RewriteToPageUrl method is implemented like this:

private void RewriteToPageUrl(UrlRewriteEventArgs e, int pageID)
{
    BasePageData page = (BasePageData)DataFactory.Instance.GetPage(new PageReference(pageID));
    if (page != null)
    {
        string path = "/" + page.ControllerName + "/" + page.PageURLSegment + "/";
        RewiteUrl(e, path);
    }
}

Both rewrite methods tries to retrieve the page with the ID specified in the querystring, and if the page exists rewrites the URL with the page’s default controller name in the first part of the URL.

The controller base class

With the URLs rewritten we will now receive requests for URL such as /article/plantation-cultivation/ and /article/52-71/ where “article” is the name of the controller. Our controllers will need to be able to figure out which PageData object we mean by “plantation-cultivation” (an article I borrowed from the Wikipedia article about bananas) or “52-71”. Therefore I created a base class with a reference to an object that will do that for us. First the base class:

using System.Web.Mvc;
using MvcTest.Web.Utilities;

namespace MvcTest.Web.Controllers
{
    public abstract class PageControllerBase : Controller
    {
        public PageControllerBase()
        {
            CurrentPageResolver = new CurrentPageResolver();
        }

        protected CurrentPageResolver CurrentPageResolver { get; set; }
    }
}

As you can see it exposes a property of the type CurrentPageResolver. The CurrentPageResolver class has a single public method, GetCurrentPage:

public BasePageData GetCurrentPage(string slug)
{
    BasePageData page = null;
    
    if (PossiblyWorkPage(slug))
    {
        page = GetWorkPage(slug);
    }

    if (page == null)
        page = GetPageBySlug(slug, PageReference.StartPage);
    
    return page;
}

private bool PossiblyWorkPage(string slug)
{
    return slug.Contains("_");
}

Just like the event listener that did the URL rewriting the GetCurrentPage method checks if the requested page is a work page (a specific version of a page, usually used in edit mode) or if it’s the published version of a page and delegates to two other methods, GetWorkPage and GetPageBySlug. GetWorkPage is implemented like this:

private BasePageData GetWorkPage(string slug)
{
    BasePageData page = null;
    
    string[] splitSlug = slug.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
    
    int pageID;
    if (splitSlug.Length > 1 && int.TryParse(splitSlug[0], out pageID))
    {
        int workPageID;
        if (int.TryParse(splitSlug[1], out workPageID))
        {
            PageReference workPageReference = new PageReference(pageID, workPageID);
            page = (BasePageData)DataFactory.Instance.GetPage(workPageReference);
        }
    }
    
    return page;
}

It tries to parse both the page ID and the work page ID and if successful returns the matching page version.

The GetPageBySlug method is implemented like this:

protected BasePageData GetPageBySlug(string slug, PageReference startingPoint)
{
    BasePageData pageToCheck = (BasePageData)DataFactory.Instance.GetPage(startingPoint);
    if (pageToCheck.PageURLSegment.Equals(slug, StringComparison.OrdinalIgnoreCase))
        return pageToCheck;

    PageDataCollection children = DataFactory.Instance.GetChildren(pageToCheck.PageLink);
    foreach (PageData child in children)
    {
        PageData matchedDescendant = GetPageBySlug(slug, child.PageLink);
        if (matchedDescendant != null)
            return (BasePageData)matchedDescendant;
    }

    return null;
}

It tries to retrieve a page with the matching URL segment. Remember that this is just prototype. For a production system the method should be implemented differently, beginning to search at the first level and then searching each level, instead of searching each branch as this implementation does. The result should also be cached.

Creating a controller

With the URL rewriting and utility methods for figuring out which page is requested done I could now create controllers and views. An example controller looks like this:

using System.Web.Mvc;
using MvcTest.Web.Models;

namespace MvcTest.Web.Controllers
{
    public class ArticleController : PageControllerBase
    {
        public ActionResult Index(string slug)
        {
            BasePageData page = CurrentPageResolver.GetCurrentPage(slug);

            return View(page);
        }

        public ActionResult Edit(string slug)
        {
            BasePageData page = CurrentPageResolver.GetCurrentPage(slug);

            return View(page);
        }

    }
}

And an example view like this:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcTest.Web.Models.Article>" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title><%= Model.PageName %></title>
</head>
<body>
    <div>
    <h3><%= Model.PageName %></h3>
    <%= Model.MainBody %>
    </div>
</body>
</html>

Source code

I’ve zipped the relevant parts of my project and put them up for download here. Note that I’ve used the CTP version of EPiServer CMS 6.

Conclusion

As I’ve mentioned earlier this is just a prototype and it’s far, far, far from perfect. It does work though, as long as all pages that have the same default controller have a unique URL segment :)

I hope I’ll be able to continue experimenting with EPiServer, MVC and Page Type Builder and will of course blog about my findings.

Any feedback is as always very welcome!

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