Archive for the ‘MVC’ Category
EPiServer and MVC
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