Fabio's blog

marketing | technology

EPiServer and MVC

with 10 comments

Thanks to the great work of Joel Abrahamsson (here his first stab at the solution) and a little more prototyping, it looks like EPiServer and MVC can play together very well! Time to say goodbye to asp.net webforms templates on EPiServer?

At Syzygy, we got this working with EPiServer 5.2.375.236 (the latest production version), MVC 1.0 and recompiled versions of the PageTypeBuilder and Castle DynamicProxy2 assemblies (the reason for this was to solve trust exceptions when the code was running from IIS 7.5).

The trick to get the EPiServer Friendly URL rewriter to play nicely with MVC is to use custom MvcHandler and IControllerFactory implementations: a wild-card style route can be passed to the custom handler which then will be able to:

  • find out from EPiServer the ID of the content item (from the friendly URL)
  • load the page data
  • read custom properties of the page data such as “ControllerName” and “ActionName”
  • forward the page data to the correct controller / action through our controller factory

So, in our prototype, we implemented the custom MVC handler with the following code:

		protected override void ProcessRequest(HttpContextBase httpContext)
		{
			var internalUrlBuilder = GetInternalUrl(httpContext.Request.RawUrl);
			var mvcPage = CurrentPageResolver.Instance.GetCurrentPage(internalUrlBuilder.QueryCollection["id"] ?? string.Empty);
			var controllerName = mvcPage.ControllerName;
			var actionName = mvcPage.ActionName;

			RequestContext.RouteData.Values.Add("action", actionName);
			RequestContext.RouteData.Values.Add("controller", controllerName);

			RequestContext.RouteData.Values["data"] = mvcPage;

			var controller = ControllerBuilder.Current.GetControllerFactory().CreateController(RequestContext, controllerName);
			controller.Execute(RequestContext);
		}

This code will intercept the request, convert the raw URL (as generated by the EPiServer Friendly URL rewriter) into the internal EPiServer URL, which will give us the ID of the content item (or, when in edit mode, the ID followed by an underscore and the version ID – i.e. “the slug”).

At this point, we can use any custom class to call the EPiServer DataFactory and get the PageData object. In our custom CurrentPageResolver class, the page data is cast to the base class for all our page types (MvcPageData), which contains information about the controller and the action to use for rendering.

using System;
using System.Collections.Generic;
using EPiServer;
using EPiServer.Core;
using MvcCms.Bases;

namespace MvcCms.Utilities
{
    public class CurrentPageResolver
    {
        private static readonly CurrentPageResolver instance = new CurrentPageResolver();

        private CurrentPageResolver()
        {
        }

        public static CurrentPageResolver Instance
        {
            get { return instance; }
        }

        public MvcPageData GetCurrentPage(string id)
        {
            if (String.IsNullOrEmpty(id))
            {
                return GetHomePage();
            }

            return IsWorkPage(id) ?
                           GetWorkPage(id) : GetPage(id);
        }

        private static bool IsWorkPage(string id)
        {
            return id.Contains("_");
        }

        private static MvcPageData GetWorkPage(string slug)
        {
            PageData 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))
                {
                    var workPageReference = new PageReference(pageId, workPageId);
                    page = DataFactory.Instance.GetPage(workPageReference);
                }
            }

            return page as MvcPageData;
        }

        protected MvcPageData GetPage(string id)
        {
            int pageId;
            if (int.TryParse(id, out pageId))
            {
                var pageReference = new PageReference(pageId);
                return DataFactory.Instance.GetPage(pageReference) as MvcPageData;
            }

            return null;
        }

        public static MvcPageData GetHomePage()
        {
            return DataFactory.Instance.GetPage(PageReference.StartPage) as MvcPageData;
        }

        public List<MvcPageData> GetChildren()
        {
            return GetChildren(GetHomePage().PageLink);
        }

        public List<MvcPageData> GetChildren(PageReference parent)
        {
            PageDataCollection pages = DataFactory.Instance.GetChildren(parent);
            var list = new List<MvcPageData>(pages.Count);
            foreach (PageData page in pages)
            {
                list.Add(page as MvcPageData);
            }
            return list;
        }
    }
}

Our page type base (MvcPageData) class looks like this:

using System.Collections.Generic;
using MvcCms.Utilities;
using PageTypeBuilder;

namespace MvcCms.Bases
{
    public abstract class MvcPageData : TypedPageData
    {
        public abstract string ControllerName { get; }
        public virtual string ActionName
        {
            get
            {
                return "Index";
            }
        }

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

        public virtual List<MvcPageData> Children
        {
            get
            {
                if (PageLink == StartPage.PageLink)
                {
                    return new List<MvcPageData>(0);
                }

                return CurrentPageResolver.Instance.GetChildren(PageLink);
            }
        }

        public virtual List<MvcPageData> SiteLinks
        {
            get { return CurrentPageResolver.Instance.GetChildren(StartPage.PageLink); }
        }

        public MvcPageData StartPage
        {
            get { return CurrentPageResolver.GetHomePage(); }
        }
    }
}

The custom controller factory will use the information that is contained in the route data to create the correct controller from the available ones. Once the controller is created, this will again be able to use the MvcPageData available in the route data (through a custom model binder), and pass it to a view.

The last step would be to wire up all the work in the global.asax, ignoring the requests to edit, admin, util and app_themes URLs.

		protected void Application_Start(Object sender, EventArgs e)
		{
			ControllerBuilder.Current.SetControllerFactory(new MvcControllerFactory());
			ModelBinders.Binders.Add(typeof (MvcPageData), new PageDataModelBinder());

			RegisterRoutes(RouteTable.Routes);
		}

		public static void RegisterRoutes(RouteCollection routes)
		{
			routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
			routes.IgnoreRoute("cms/{*pathInfo}");
			routes.IgnoreRoute("util/{*pathInfo}");
			routes.IgnoreRoute("app_themes/{*pathInfo}");

			routes.Add(new Route("{*data}", new WildCardRouteHandler()));
		}

In order to get the solution working, you’ll need to follow the instructions in the readme.txt file. Any problems, contact me.

Enjoy!

Fabio

Written by Fabio

October 10, 2009 at 1:21 pm

Posted in .NET, EPiServer, MVC

10 Responses

Subscribe to comments with RSS.

  1. Very interesting reading Fabio!

    Creating a custom MVC handler is an approach that I haven’t looked into myself yet so I found that especially interesting.

    Have you thought anything about how to handle other actios than the default? I’m thinking about scenarios such as a page where visitors can comment and editors can remove comments. In that scenario our controller needs to have three different actions while it’s the same PageData object.

    It might be me that have misunderstood it, but as I understand it you have a one-to-one relationship between page types and actions in your solution?

    Joel Abrahamsson

    October 12, 2009 at 5:15 pm

    • Hi Joel, you are right: a one-to-one relationship between page types and actions, in this scenario… But that is the nature of the {*data} wildcard mapping, unless some action patterns are hardcoded (i.e. {*data}/edit, {*data}/postcomment, etc.) which could create problems on page with that url segment! In a scenario like that, using the URL patterns with the EPiServer identity provider would be probably much better.

      Cheers
      Fabio

      Fabio

      October 12, 2009 at 7:34 pm

  2. […] EPiServer and MVC « Fabio's blog fbrz.wordpress.com/2009/10/10/episerver-and-mvc – view page – cached Thanks to the great work of Joel Abrahamsson (here his first stab at the solution) and a little more prototyping, it looks like EPiServer and MVC can play together very well! Time to say goodbye to… (Read more)Thanks to the great work of Joel Abrahamsson (here his first stab at the solution) and a little more prototyping, it looks like EPiServer and MVC can play together very well! Time to say goodbye to asp.net webforms templates on EPiServer? (Read less) — From the page […]

  3. Great post! Getting MVC to play nicely with EPiServer would be a big step forward.

    What changes would need to be done to your project to get it running on EPiServer 6 and IIS 7.5?

    Regards,
    Peter

    Peter

    April 16, 2010 at 7:44 am

    • Hi Peter,

      I never tried with the final version of Episerver 6, but an initial prototype on an earlier beta was working as well as 5.2.
      We are running our company site using the MVC integration code deployed on IIS 7.5. No particular issues (just ensured that the IIS7 modules and handlers configuration is correct), and the site runs great!

      Fabio

      April 17, 2010 at 2:34 pm

  4. […] online to see what others had done. I found two great posts by Joel Abrahamsson and one from Fabio Fabrizio, who based his solution on Joels […]

  5. What would you say the biggest limitations are with this solution?

    Cheers,
    Peter

    Peter

    April 22, 2010 at 2:24 pm

    • I think that the biggest limitation is the “FriendyUrlRewriter”: if you want to keep the site structure matching the EPiServer content tree, then the only MVC route is the greedy one ({*data}). With this, you end up with a one to one relationship between pages and controller actions, as pointed out by Joel in his previous comment.

      If you have a site structure predefined, which deviates from the EPiServer content tree, then you’ll get more flexibility using a different URLRewriter (e.g. the EPiServer “IdentityRewriter”, which uses the page IDs directly): that way you could have multiple actions on every page.

      Cheers
      Fabio

      Fabio

      April 22, 2010 at 3:07 pm

      • I am experimenting with an approach that would allow us to use FriendyUrlRewriter and have a one to many relationship between pages and actions.

        Basically what my code does is to strip of the end of the URL until it finds a page. For example this url:

        1. http://www.mysite.com/Products/Edit/1 -> No page found
        2. http://www.mysite.com/Products/Edit/ -> No page found
        3. http://www.mysite.com/Products/ -> Products page found, Action=Edit, params=1

        Of course, this has the obvious limitation that if you have a child page to the page it will have higher presidence than the Edit action. To avoid this I am thinking about hooking on to the Page Save event and Page Move event in EPi to check if the parent page has Actions that corresponds to the friendly URL of the page.

        What do you think of this approach?

        Peter

        April 23, 2010 at 6:57 am

      • Hi Peter, sorry I’ve been away from this site for a while. I’m pretty sure in the meantime you must have come across this: http://joelabrahamsson.com/entry/epimvc-a-framework-for-using-episerver-cms-with-aspnet-mvc.
        Cheers,
        Fabio

        Fabio

        December 17, 2010 at 4:14 pm


Leave a reply to Joel Abrahamsson Cancel reply