One page type, 2 sites, 2 controllers and 2 views in EPiServer 7.5

Mattias Olsson 2014-03-20 01:21:41

First I created a custom ViewEngine to be able to specify view locations based on the current site (SiteDefinition.Current.Name):

/// <summary>
/// Extends the Razor view engine to set custom view locations.
/// </summary>
public class MultipleSiteViewEngine : RazorViewEngine
{
    public MultipleSiteViewEngine()
    {
        string viewsFolder = SiteDefinition.Current.Name;
 
        ViewLocationFormats = new[]
			{
				string.Concat("~/Views/"viewsFolder"/{1}/{0}.cshtml"),
				string.Concat("~/Views/"viewsFolder"/Shared/{0}.cshtml"),
				"~/Views/Common/{1}/{0}.cshtml",
				"~/Views/Shared/{0}.cshtml"
			};
 
        PartialViewLocationFormats = new[]
			{
				string.Concat("~/Views/"viewsFolder"/{1}/{0}.cshtml"),
				string.Concat("~/Views/"viewsFolder"/Shared/Partials/{0}.cshtml"),
				string.Concat("~/Views/"viewsFolder"/Shared/Blocks/{0}.cshtml"),
				string.Concat("~/Views/"viewsFolder"/Shared/{0}.cshtml"),
				"~/Views/Common/{1}/{0}.cshtml",
				"~/Views/Shared/Blocks/{0}.cshtml",
				"~/Views/Shared/{0}.cshtml"
			};
    }
}

I had to register the view engine on first request because a HttpRequestBase is required for the SiteDefinition.Current to be resolved correctly. Feel free to let me know of a better way of doing this. This is the code used in Global.asax.cs.

private static volatile bool _isFirstRequest;
private static readonly object _lockObj;
protected void Application_BeginRequest(object sender, EventArgs e)
{
    if (_isFirstRequest)
    {
        return;
    }
 
    lock (_lockObj)
    {
        if (!_isFirstRequest)
        {
            _isFirstRequest = true;
 
            // Add custom view engine. It is important that we do this at first request 
            // instead of Application_Start since EPiServer.Web.SiteDefinition.Current 
            // requires a HttpRequestBase to be able to resolve the correct site.
            ViewEngines.Engines.Insert(0, new MultipleSiteViewEngine());
        }
    }
}

For EPiServer to select the correct controller for a given page type based on what site you're browsing I added a TemplateDescriptor attribute to the controllers for each site:

[TemplateDescriptor(Name = "MyFirstSiteName")]
public class StandardPageController : PageController<StandardPage> 
{
}
[TemplateDescriptor(Name = "MySecondSiteName")]
public class StandardPageController : PageController<StandardPage> 
{
}

You can of course just have one single controller handling all sites as well. Then you don't need the TemplateDescriptor attribute.

To wrap things up I created an initialization module to hook up to the TemplateResolver and select the correct controller based on my TemplateDescriptor attribute:

/// <summary>
/// Module for customizing templates and rendering.
/// </summary>
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class CustomizedRenderingInitialization : IInitializableModule
{
	public void Initialize(InitializationEngine context)
	{
		context.Locate.TemplateResolver()
			.TemplateResolved += OnTemplateResolved;
	}
 
	protected virtual void OnTemplateResolved(object sender, TemplateResolverEventArgs args)
	{
		if (args.SupportedTemplates == null || args.ItemToRender == null || args.ItemToRender is IContainerPage)
		{
			args.SelectedTemplate = null;
			return;
		}
 
        if (SiteDefinition.Current == null || string.IsNullOrEmpty(SiteDefinition.Current.Name))
        {
            return;
        }
 
		var renderer =
			args.SupportedTemplates.SingleOrDefault(
				r =>
                r.Name != null &&
				r.Name.Equals(SiteDefinition.Current.Name, StringComparison.OrdinalIgnoreCase) &&
				r.TemplateTypeCategory == args.SelectedTemplate.TemplateTypeCategory);
 
		if (renderer == null)
			return;
 
		args.SelectedTemplate = renderer;
	}
 
	public void Uninitialize(InitializationEngine context)
	{
		ServiceLocator.Current.GetInstance<TemplateResolver>()
			.TemplateResolved -= OnTemplateResolved;
	}
 
	public void Preload(string[] parameters)
	{
	}
}

Doing all of this allows us to have our Views folder look something like this, given that you have two sites defined in EPiServer, the first with the name "MyFirstSiteName" and the second with the name "MySecondSiteName":

~/Views/MyFirstSiteName/ -> Views, layouts and partial views specific to first site.
~/Views/MySecondSiteName/ -> Views, layouts and partial views specific to second site.
~/Views/Common/ -> Views common to both sites.
~/Views/Shared/ -> Views and partial views common to both sites.

To select the correct Layout we have this code in ~/Views/_viewstart.cshtml:

@using EPiServer.Web
@{
    Layout = string.Format("~/Views/{0}/Shared/Layouts/_Layout.cshtml", SiteDefinition.Current.Name);
}

Heads up: For this to work the sites needs to have their own application pool in IIS. Otherwise the custom ViewEngine will be registered when the first requested site loads and the ViewLocations for that site will be used for all sites. This is not good in our case.