Pages

Wednesday, March 18, 2015

Organizing Views in Sitecore

I have recently been working on a Sitecore project that is not terribly complicated, but has the potential to support multiple sites. The sites are similar in structure, and they will share some components, but there are many pieces of them that will be unique enough to warrant their own Views.

And with that magic word you can probably guess that this Sitecore instance is built with MVC. For a little more information about it, this instance of Sitecore is running version 7.5 and MVC 5.1 with Razor syntax.

The Problem

The problem is I want to keep an organized structure for my site not only with my Sitecore architecture, but also for my Controllers and Views. I am dealing with a Sitecore instance that will host multiple sites in the near future, so I have structured Sitecore to have different site-specific folders in my Content, Layouts, Media Library, and Templates sections. There are also Global folders in these sections where I will store items that are universal, but the specifics around my architecture can be saved for another post.

Along with this structure in Sitecore, I would like to organize my Controllers and Views in a similar fashion to have site-specific subfolders within my site directory. However, due to the MVC approach to Controller and View mapping, I can’t just create subfolders within my Controllers and Views folders and expect the ViewEngine to magically find them.

The MVC Approach to Views

Anyone who has worked with MVC probably knows that a specific structure and naming convention is key for the mapping between Controllers and Views. By default, MVC stores Controllers in the Controllers folder, and Views in the Views folder. That’s pretty straight forward. Additionally, the default convention for views follows:
  • ~/Views/[Controller]/[Action]
 Given a controller named “TestController”, and an Action named “GetTest”, the predefined locations the default ViewEngine will look for a “GetTest” file are:
  • ~/Views/Test – matches the Controller name
  • ~/Views/Shared – default shared layouts location
It doesn’t matter how many subfolders you have in your Controllers or Views folders, those default locations are always searched according to the standard naming convention. To change this behavior you have to either specify a direct path to the View wherever you reference it, or define your own ViewEngine.

My Views Organization

Before arriving at a solution, I first defined my folder structure I wanted for this project. Since I am dealing with Sitecore, my Views relate to Layouts and Renderings in the system. I have therefore defined my Views into 3 groups for now:
  1. Layouts – There is only a Main layout for now, but should we need a completely separate look, I have this.
  2. Containers – These relate to Header, Footer, Main content areas, or other structures that contain mostly Sitecore placeholders. The purpose of these is to define the page structure: rows, columns, etc…
  3. ContentItems – These are simple content items displaying Sitecore fields, or custom types like Navigation, Accordion, Sliders, etc....
I could break this structure down further for specificity at some point, but these sites don’t need it. Additionally, I can keep a Global folder with the same structure for any components that can be used across the sites. This structure is also general enough that I can use the same folder structure for each site I have in Sitecore.

To keep things simple and uniform, this same folder structure is how I defined my Layouts and Renderings in Sitecore.

My Solution

With my directory structure in mind, I wanted to simplify my work while accounting for new sites in the future. I also wanted to take advantage of ViewEngines and their ability to eliminate my need for specifying every View path in my Controllers or Views. To do this, I needed to create a custom ViewEngine.

For a custom ViewEngine, we need to define the locations where our Views can reside. These are known as location formats, and the RazorViewEngine defines two sets: PartialViewLocationFormats and ViewLocationFormats, defined as string arrays. An example of a default Razor location format is:
  • ~/Views/{1}/{0}.cshtml – {1} is the Controller name and {0} is the Action name
Based on my structure, my location formats are defined as the following:
  • ~/Views/[SITE FOLDER]/Containers/{0}.cshtml
  • ~/Views/[SITE FOLDER]/ContentItems/{0}.cshtml
  • ~/Views/[SITE FOLDER]/Layouts/{0}.cshtml
The catch with this is all 3 are needed for each site, which can get unwieldy in code, and become difficult to manage. Additionally, I don't want to have to rebuild my solution every time I add or remove a site.

To make this more flexible, with more ease to adding and removing sites from the list, I created a key in the Web.config AppSettings with a comma delimited list of my site names as the value.
[pre class="brush: xml" title="Web.config"]
<appSettings>
    <!-- comma delimited name of websites being used-->
    <add key="Websites" value="Test1,Test2"/>
</appSettings>[/pre]

In my CustomViewEngine (very original name) I then split the value of the “Websites” key, and loop through each, adding the proper location format to my list. I build and return this array of location formats in a separate function called "getSiteLocationFormats". Additionally, my folder names are stored in a string array for me to loop through for ease of maintenance.
[pre class="brush: csharp" title="getSiteLocationFormats"]
private string[] getSiteLocationFormats() {
    var sitesString = ConfigurationManager.AppSettings["Websites"];
    string[] folders = { "Containers", "ContentItems", "Layouts" };
    List<string> locations = new List<string>();

    if (sitesString != null && !string.IsNullOrWhiteSpace(sitesString)) {
        foreach (var site in sitesString.Split(',')) {
            foreach (var folder in folders) {
                locations.Add(string.Format("~/Views/{0}/{1}/{{0}}.cshtml", site, folder));
            }
        }
    }
    return locations.ToArray();
}
[/pre]

The location formats then have to be specified on the ViewEngine and this is done by setting the PartialViewLocationFormats and ViewLocationFormats. You can specify one or both depending on your need, but I wanted all views in these locations so I specified both. This step, plus the call to my "getSiteLocationFormats" function is done in the constructor:
[pre class="brush: csharp" title="CustomViewEngine constructor"]
public CustomViewEngine() {
    var locationFormats = getSiteLocationFormats();

    PartialViewLocationFormats = PartialViewLocationFormats.Union(locationFormats).ToArray();
    ViewLocationFormats = ViewLocationFormats.Union(locationFormats).ToArray();
}
[/pre]

After that, you simply register the ViewEngine. This is done in the Application_Start method of the Global.asax:
[pre class="brush: csharp" title="Global.asax.cs"]
protected void Application_Start() {
    ViewEngines.Engines.Add(new Custom.Configuration.CustomViewEngine());

    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}[/pre]

Some people clear out the default ViewEngines before adding the new one, others leave it. I don't think it's a significant performance hit in my case, nor do I know, if Sitecore is doing anything that would be affected by me doing it, so I just left it and added my new one.

With this approach in place, whenever I need to add a new website, I can modify the comma delimited list in my Web.config, add my site folders to my Views directory, and start building my Controllers and Sitecore Renderings.

No comments:

Post a Comment