Sitecore with MVC Areas as pluggable Module

This post is first in series to describe the features provided by framework for Sitecore Mvc. For complete list and detail please see main post.

I have been working on Sitecore Mvc since its Technical Preview version and we were the first few early adopter of Mvc version. My Team had good or I would say rather better architecture laid out to handle Sitecore, Mvc areas, Helper library, services and other layers in the solution. I moved on but need of similar framework comes again and again. During this entire duration I have seen several post on Sitecore with areas implementation on SO and elsewhere.

Pluggable module concept is based on approach suggested by Sean Kearney back in 2010 and I believe it should be the standard model for Sitecore development as it reduces complexity and keeps the code clean from other dependencies. On the other hand Sitecore’s support for Mvc areas is limited and it requires hard coding of view path. Kevin Buckley came out with additional package to support the Mvc areas and solution described in this post is build on top of it.

Pluggable area has helped me to run an isolated instance of site with lower possible combination of functionality, spinning up new site with only certain section of the application, resolving performance issue of the application by isolating the section of the site.

Sitecore Mvc Execution cycle

David Morrison has posted the block diagram giving the details how Sitecore executes the Mvc Controller. Sitecore uses GetControllerRenderer processor pipeline to emit the response after controller execution. This pipeline reads Controller & Action name from controller rendering definition and hands over to ControllerRenderer which intern calls ControllerRunner to execute the controller’s action method and emit the response.

Solution

As Sitecore still uses underline Mvc infrastructure to create the controller it is easy to inject the area name to build the support for Mvc Areas. To avoid conflict between duplicate controller names it make sense to inject namespace name also in route data. To build the pluggable Mvc areas support this solution has 2 major component.

  1. Pluggable Mvc Areas
  2. Build support for Sitecore
    1. Sitecore Template
    2. Code/Configuration

Pluggable MVC Area

The built-in Project template in Visual studio allows areas to be added as part of the web project. But if you are designing the enterprise project with several dependencies and large team it becomes impossible to have everything in one single web project, this can be avoided by creating separate web project for every areas. The only caveat to this approach is to have a class with AreaRegistration implementation.

Area Project Solution structure

This approach requires two changes.

  1. Deploy the Area project DLL to Host application bin folder
  2. Copy the Area project View files to Host application’s area folder

Deploy the Area project DLL to Host application bin folder

After build Area project DLL can be dropped inside the host application’s bin folder either using post build script or by setting the build output path (Project Properties->Build->Output->Output Path) to host app’s bin folder. On call of AreaRegistration.RegisterAllAreas() Mvc’s BuildManager class scan for all the assembly and looks for AreaRegistration implementation, if it is available then area route will get registered in the route table.

This approach is just for PoC and it can be very easily changed to nuget package during deployment.

Build Output Path

Update 10-30-2014

The above approach resulted me loosing the intellisense in my view files and I decided to copy the DLLs as well via XCOPY script. In fact, it’s much better as I can do a web publish of my area project and drop the files anywhere.

xcopy "$(TargetDir)*.*" "$(SolutionDir)host\bin\" /s /i /y

Copy the Area project View files to Host application’s area folder

This is required for Mvc to locate the View files and I am currently using post build xcopy to copy the file to Host App’s area folder.

xcopy “$(ProjectDir)Views” “$(SolutionDir)Host\Areas\$(ProjectName)\Views\” /s /i /y

Post Build XCopy

Sitecore Template

To hold the Area and Namespace name with controller rendering, I have created a new template which accepts Area Name and Namespace name.

Area Controller Rendering Template

Just remember the Template id of new Form template which is required to be passed to Sitecore via configuration while executing the controller renderer. It has Form Section as well on the template and that is required for Mvc Form Post support but it is not mandatory. Now assign Sitecore’s Controller Rendering as Base Template for this template.

Area Controller Rendering Template with Base Template

You can define the Insert option on Rendering folder for newly created template to allow creation of renderings quickly without referring the template again.

Area Controller Rendering as Insert option on rendering folders

Code/Configuration

Below are the classes which requires the extension

AreaControllerRendererProcessor

This class is responsible to create the instance of Area Controller Renderer by passing the controller, action, area and namespace name.

public class AreaControllerRendererProcessor : GetRendererProcessor
    {
        public virtual string TemplateId { get; set; }

        public override void Process(GetRendererArgs args)
        {
            if (args.Result != null)
            {
                return;
            }
            
            Template renderingTemplate = args.RenderingTemplate;
            if (renderingTemplate == null)
            {
                return;
            }
            if (!renderingTemplate.DescendsFromOrEquals(new ID(TemplateId)))
            {
                return;
            }
            args.Result = this.GetRenderer(args.Rendering, args);
        }

        protected virtual Renderer GetRenderer(Rendering rendering, GetRendererArgs args)
        {
            string action = rendering.RenderingItem.InnerItem.Fields["controller action"].GetValue(true);
            string controller = rendering.RenderingItem.InnerItem.Fields["controller"].GetValue(true);
            string area = rendering.RenderingItem.InnerItem.Fields["area"].GetValue(true);
            string namespaceNames = rendering.RenderingItem.InnerItem.Fields["namespace"].GetValue(true);

            return new AreaControllerRenderer
            {
                Action = action,
                Controller = controller,
                Area = area,
                Namespaces = namespaceNames
            };
        }
    }

AreaControllerRenderer

This class is responsible to call the ControllerRunner class by passing controller, action, area, and namespace and retrieve the result to write on response stream.

 public class AreaControllerRenderer : Renderer
    {
        public string Action { get; set; }
        public string Controller { get; set; }
        public string Area { get; set; }
        public string Namespaces { get; set; }

        public override string CacheKey
        {
            get
            {
                return "areacontroller::" + Controller + "#" + this.Action + "#" + Area + "#" + Namespaces;
            }
        }

        public override void Render(System.IO.TextWriter writer)
        {
            AreaControllerRunner controllerRunner = new AreaControllerRunner(this.Controller, this.Action, this.Area, this.Namespaces);

            string value = controllerRunner.Execute();
            if (string.IsNullOrEmpty(value))
            {
                return;
            }
            writer.Write(value);
        }

        public override string ToString()
        {
            return "Controller: {0}. Action: {1}. Area {2}. Namespaces {3}".FormatWith(new object[]
			{
				this.Controller,
				this.Action,
                this.Area,
                this.Namespaces
			});
        }
    }

AreaControllerRunner

Responsibility of this class is to assign the route data values before executing the controller to create the correct instance and execute it. After execution is finished restore the original values back in route data.

public class AreaControllerRunner : ControllerRunner
    {
        public AreaControllerRunner(string controllerName, string actionName, string area, string namespaceNames)
            : base(controllerName, actionName)
        {
            this.Area = area;
            this.ControllerName = controllerName;
            this.ActionName = actionName;
            this.Namespaces = namespaceNames;
        }

        public string Area { get; set; }
        public string Namespaces { get; set; }

        protected override void ExecuteController(System.Web.Mvc.Controller controller)
        {
            RequestContext requestContext = PageContext.Current.RequestContext;
            object value = requestContext.RouteData.Values["controller"];
            object value2 = requestContext.RouteData.Values["action"];
            object value3 = requestContext.RouteData.DataTokens["area"];
            object value4 = requestContext.RouteData.DataTokens["namespace"];

            try
            {
                requestContext.RouteData.Values["controller"] = this.ActualControllerName;
                requestContext.RouteData.Values["action"] = this.ActionName;
                requestContext.RouteData.DataTokens["area"] = this.Area;
                requestContext.RouteData.DataTokens["namespace"] = this.Namespaces.Split(new char[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries);
                ((IController)controller).Execute(PageContext.Current.RequestContext);
            }
            finally
            {
                requestContext.RouteData.Values["controller"] = value;
                requestContext.RouteData.Values["action"] = value2;
                requestContext.RouteData.DataTokens["area"] = value3;
                requestContext.RouteData.DataTokens["namespace"] = value4;
            }
        }
    }

Once everything is ready, plug it in with configuration in Sitecore execution pipeline.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.getRenderer>
        <processor patch:before="processor[@type='Sitecore.Mvc.Pipelines.Response.GetRenderer.GetViewRenderer, Sitecore.Mvc']" type="Framework.Sitecore.Extensions.MvcAreas.Pipelines.GetRenderer.AreaControllerRendererProcessor, Framework.Sitecore.Extensions" >
          <templateId>{D7018B2D-AE1F-47C0-B2ED-AC5BAFFB3C27}</templateId>
        </processor>
      </mvc.getRenderer>
    </pipelines>
  </sitecore>
</configuration>

That’s all is required to support the pluggable areas in Sitecore Mvc. Any feedback please drop a comment or mail me.

Advertisements

About cprakash

A developer, passionate about .Net, Architecture and Security.
This entry was posted in Sitecore and tagged , , . Bookmark the permalink.

7 Responses to Sitecore with MVC Areas as pluggable Module

  1. Pingback: Sitecore 8: Fix MVC area execution and registration | cprakash

  2. Pingback: Exception Handling in Sitecore MVC | cprakash

  3. KD says:

    hi Chandra,
    Actually I posted a comment on a wrong thread earlier.

    Is this solution still applies to Sitecore 8.1 with MVC area is supported out of the box?

    Thanks,
    KD

    • cprakash says:

      Hi KD,

      No, you don’t need this solution, in case if you are using the complete framework you can just disable the Pipeline in the framework configuration file. To be more precise it’s AreaControllerRendererProcessor processor under mvc.getRenderer pipeline in Framework.SC.Extensions.MvcAreas.config file.

      Thanks,

  4. Venkatesh says:

    Hi Prakash
    I want to configure multisite with areas in MVC. Is it good to follow the approach
    ? Please let me know how to implement it and also on how to handle sessions, custom events and custom pipelines in multi site setup?

    • cprakash says:

      Hi Venkatesh,

      Yes, you can use this approach to setup multi site and that should work. If you know of then you can try Sitecore Habitat as well.

      To configure multi site, you can add additional configuration in section for website. For rendering just provide Namespace, area, controller and action name to run the corresponding code.

      Session is same as like asp.net application, just make sure, you externalize it via a ISessionService interface for better unit test and DI. Custom events and pipelines are based on your needs and requirement.

      Feel free to reach out if you need additional help.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s