Views: 10818
Number of votes: 1
Average rating:

Rendering EPiServer MVC request – Part 1

Now when our MVC support finally has been released, I thought I would write a blog post about how we handle the request.

Adding types, controller, model, and views

Before showing what we are doing with the request, we need content types, controllers, and views.

Content types

First we add a basic page type and a basic block type.

using EPiServer.DataAnnotations;
using EPiServer.Core;
 
namespace Basic.Web.ContentTypes
{
    [ContentType]
    public class BasicPage : PageData
    {
        public virtual string Heading { get; set; }
        public virtual XhtmlString MainBody { get; set; }
        public virtual BasicBlock MyBlock { get; set; }
    }
}
using EPiServer.Core;
using EPiServer.DataAnnotations;
 
namespace Basic.Web.ContentTypes
{
    [ContentType]
    public class BasicBlock : BlockData
    {
        public virtual string Heading { get; set; }
        public virtual XhtmlString MainBody { get; set; }
    }
}

Viewmodels

Some basic models for the page type and the block type.

using EPiServer.Core;
using FlyFind.Web.ContentTypes;
 
namespace Basic.Web.Models
{
    public class BasicPageModel
    {
        public string Heading { get; set; }
        public XhtmlString Body { get; set; }
        public BasicBlock MyBlock { get; set; }
    }
}
using EPiServer.Core;
 
namespace Basic.Web.Models
{
    public class BasicBlockModel
    {
        public string Heading { get; set; }
        public XhtmlString Body { get; set; }
    }
}

Controllers

We start with adding a controller for the Basic page, and uses the generic “PageController” as base class.

using System.Web.Mvc;
using FlyFind.Web.ContentTypes;
using EPiServer.Web.Mvc;
using FlyFind.Web.Models;
 
namespace Basic.Web.Controllers
{
    public class BasicPageController : PageController<BasicPage>
    {
        [ContentOutputCache]
        public ActionResult Index(BasicPage currentPage)
        {
            var model = new BasicPageModel { Heading = currentPage.Heading, Body = currentPage.MainBody, MyBlock = currentPage.MyBlock };
 
            var editingHints = ViewData.GetEditHints<BasicPageModel, BasicPage>();
            editingHints.AddConnection(x => x.Body, x => x.MainBody);
 
            return View(model);
        }
    }
}

Becouse we are using different name for the XhtmlString in the view model, we have to create an edit hint to make the on page edit work.

We also add the “ContentOutputCache” to the action to make sure it’s cached. This is very important when using local blocks or content areas on a page, where the sub content has controllers. For every sub content with a controller (it’s possible to have a page or block with only a view), we will make a new request to render the sub content. This costs. By using the “ContentOutputCache” we will cache the output until someone publishing something in the CMS. If you can’t use the “ContentOutputCache”, for example when using child actions (RenderAction), consider skipping the controller for the block.

Then we have the block controller.

using System.Web.Mvc;
using FlyFind.Web.ContentTypes;
using EPiServer.Web.Mvc;
using FlyFind.Web.Models;
 
namespace Basic.Web.Controllers
{
    public class BasicBlockController : BlockController<BasicBlock>
    {
        public override ActionResult Index(BasicBlock currentBlock)
        {
            var model = new BasicBlockModel { Heading = currentBlock.Heading, Body = currentBlock.MainBody };
 
            var editingHints = ViewData.GetEditHints<BasicBlockModel, BasicBlock>();
            editingHints.AddConnection(x => x.Body, x => x.MainBody);
 
            return PartialView(model);
        }
    }
}

Views

We add basic views for this example.

@using EPiServer.Web.Mvc.Html
@model Basic.Web.Models.BasicPageModel
 
<h2>Page</h2>
@Html.PropertyFor(x => x.Heading)
@Html.PropertyFor(x => x.Body)
 
<h2>Block</h2>
@Html.PropertyFor(x => x.MyBlock)
@using EPiServer.Web.Mvc.Html
@model Basic.Web.Models.BasicBlockModel
 
@Html.PropertyFor(x => x.Heading)
@Html.PropertyFor(x => x.Body)

Creating a page of the page type

Lets creating a page and see how it renders.

image

image

image

image

Ye, now we have something rendered, so lets see how it was rendered.

Rendering the content

As you can see from the last screenshot, the url to the page are “/MyBasicPage”. We could also have added “en” before the page name to specify that the page should be rendered in English.

Route registrations

In the global base, we register several routes to support several scenarios of incoming requests. A typical requests looks like:

"{language}/{node}/{partial}/{action}".

ContentRoute

When the request comes to the server, the ContentRoute (EPiServer.Web.Routing.ContentRoute) class, which inherits from System.Web.Routing.Route, will try to handle the request for each registered content route. We overrides the two methods “GetRouteData” and “GetVirtualPath”. GetVirtualPath handles outgoing paths, and will not be covered in this blog post. The “GetRouteData” handles incoming requests, and this is the starting point for us.

GetRouteData

We start by getting the “AppRelativeCurrentExecutionFilePath” from the request. This is the raw string that will be parsed to find the correct page, language, action and so on.

Then we’re sending an event, making it possible for a listener to cancel further routing. This makes it possible to handle the whole routing for a listener.

As long there’s no listener that cancel further routing, we will start matching the segments for the route with with a segment context class that holds the raw string. The segments for "{language}/{node}/{action}" are language, node, partial, and action.

Language segment

The language segment will look if the first segment in the url (in our case “/MyBasicPage” are a listed enabled language in the “LanguageBranchRepository”. If it is, that part of the url will be removed from the segment context class. Otherwise (as in this case), we will not remove anything, and instead look if the route contains any default language. Then will the language be set both in the segment context class, and the “ContentLanguage” singleton instance.

The Language segment will never return false, which would indicate that the route doesn't match the url. When a segment returns false, the route will not continue, and the next registered route will get it chance.

Node segment

Next segment in our route is the node segment. We will now get the next segment from the url (“MyBasicPage”) and look if there is a page with the segment property set to “MyBasicPage” directly under the root page. In this case it is, and then it will take the content reference and hold it in the memory. Then the “MyBasicPage” will be removed from the segment context.

Now we will continue to look at the next segment to see if that one is a child content to the page we just received. In this case, there’s no more segment, so we will set the content reference in the memory to the segment context and return true, indicating that the segment was valid.

Parameter segment

The action in the route falls under the “ParameterSegment”, which is a simple segments that set the next value, if it exists, in the value collection of “RouteData”. If there wasn't any more segment in the url (as in our case), it will look if there is a default value set for the segment in the route. In our case the default action was set to “index”.

It there is no more segment in the url, and there is no default value for the segment in the route, the parameter segment will return false, which make the route fail, and the next route will get it’s chance.

On routed content event

At the end of the method, we will send the routed values to listeners of the “RoutedContent” event.

MultiplexRoutingHandler

The “IRouteHandler” called “MultiplexRoutingHandler” will try to handle both webforms and MVC requests. When the method “GetRouteHandler” on the type are called, we will receive the routed data from the “ContentRoute”, and call the “TemplateResolver”, which will try to find the controller which will handle the request.

TemplateResolver

The template resolver will try to find a controller of the content type that was found in the “ContentRoute”. In CMS 7, there is a possiblity to have several controllers that supports a content type, and therefor we have rules what controller that will be picked.

Template resolver algorithm

The algorithm part has been stolen from Johan Björnfoots great blog post about rendering of content.

The algorithm to select a template is handled by ITemplateResolver and the default implementation is something like:

  1. First event EPiServer.Web.ITemplateResolver.TemplateResolving is raised, if an event handler selects a template that template is used with no further handling.
  2. Else all the templates matching a type is filtered according to if the rendering is a page rendering (in that case suitable templates are WebForm or MVC controller) or a partial rendering (in that case a suitable template is a WebControl, UserControl, MVC partial controller or a MVC partial view). For partial renderers the list is filtered according to main renderer, that is if the main request is a e.g. handled by a WebForm then only partial web form renderers are taken into account for partial renderer and the same applies if main renderer is an MVC renderer then only partial MVC renderers are taken into account.
  3. If the template is requested with a specific tag the list is filtered on that tag (e.g. can rendering of a ContentArea be tagged with e.g. “SideBar” and then renderers with matching tag is preferred).
  4. If no template matched tag continue with all templates from point 2.
  5. If any DisplayChannel is active and there are templates with a Tag matching the active channel the templates are filtered to the ones matching the DisplayChannel.
  6. From the remaining templates select the “closest” TemplateModel that is marked as Default (can be set on RenderDescriptor attribute) and not inherited.
  7. If no match from 5 select “closest” TemplateModel that is marked as Default.
  8. If no match from 6 select “closest” TemplateModel.
  9. Event EPiServer.Web.ITemplateResolver.TemplateResolved is raised, giving chance to replace selected template.

Setting the controller

When using a normal MVC site, the controller will be found by conventions. This is not the case for a controllers in the CMS, which uses IRenderTemplate (which PageController implements). We already know which controller to render after we have used the template resolver, so we will set the controller type in the DataToken directly.

Returning the handler

When we have set the controller, we will tell the system to use the MvcRouteHandler, and return the result of the “GetHttpHandler” for the handler with the request context that we have modified (most important, we have set the controller type).

Part 2

In part 2, which will be created soon, I will continue with this example, and explain how we handles the properties, how we connect the properties to the on page edit and so on.

Nov 02, 2012

Johan Kronberg
(By Johan Kronberg, 11/2/2012 2:15:51 PM)

Why not have a BasicPage property in the view model that you set to currentPage instead of duplicating Heading and Body like you're doing now?

jonas.bergqvist
(By jonas.bergqvist, 11/2/2012 2:44:51 PM)

Johan: This example is not made to be a good example for real world. I want to explain how the rendering of blocks works, and I will write about on monday. The block is only there to demonstrate BlockController and rendering of it.

shahid.nawaz
(By shahid.nawaz, 12/13/2012 3:50:35 PM)

FYI,
In Release7 RenderDescriptor has been renamed with TemplateDescriptor

Please login to comment.