View Locations

Spark file patterns

Spark will locate files using a very conventional pattern. These patterns will not change no matter which view folders are configured.

For controller views, the name of the controller and the name of the view are combined, like home\index.spark. If that file does not exist the name of the view is looked for in the Shared folder, like shared\index.spark.

For master layout templates, the name of the master will looked for in the Layouts folder, like layouts\application.spark. If that file does not exist it too will be looked for in the Shared folder, like shared\application.spark.

These patters will be looked for inside the view folder implementations. The view folders will never alter that part of the Spark file pattern, and the patterns won't affect the location of the view folders. This is very different from the behavior of the WebForms view engine which allows you to provide an array of customized virtual paths.

Sorry, but it was simply impossible to give the view engine full control of the entire path and remain compatible with the different frameworks and existing usage. The customized virtual paths are also incompatible with some of the specialized view folder sources.

Extending file patterns with descriptor filters

This is an ASP.NET MVC specific feature. In MonoRail the framework locates the view files and the paths are provided explicitly.

The SparkViewFactory uses a IDescriptorBuilder service to turn master, controller, and view names into a SparkViewDescriptor. The DefaultDescriptorBuilder provides a mechanism to add any number of IDescriptorFilter implementations to modify the potential locations of a view using information from the current request context.

There are three build-in implementations of IDescriptorFilter: AreaDescriptorFilter, LanguageDescriptorFilter, and ThemeDescriptorFilter. All three are optional, but the AreaDescriptorFilter is added by default. The other two require the web application to provide the mechanism that identifies a theme or language for a request.

Note: this descriptor filters only alter the search paths for views, layouts, and asp.net partial views. It does not change the search paths for _filename.spark partial files.

Here is example of adding a language filter. The LanguageDecriptorFilter is an abstract class, so you either need to inherit and implement or use the static method For which takes a lambda expression or method reference.

public static void RegisterViewEngine(ICollection<IViewEngine> engines)
{
    var services = SparkEngineStarter.CreateContainer();
    services.AddFilter(LanguageDescriptorFilter.For(GetSessionCulture));
    SparkEngineStarter.RegisterViewEngine(services);
}
public static string GetSessionCulture(ControllerContext controllerContext)
{
    return Convert.ToString(controllerContext.HttpContext.Session["culture"]);
}

AreaDescriptorFilter

The area filter by default detects an {area} RouteData.Value. It is added as leading folder.

For example, with Area:Sales, Controller:Foo, and View:Bar the locations:

  • Foo/Bar.spark
  • Shared/Bar.spark

become:

  • Sales/Foo/Bar.spark
  • Sales/Shared/Bar.spark
  • Foo/Bar.spark
  • Shared/Bar.spark

ThemeDescriptorFilter

The theme filter requires a lambda expression or method reference at a minimum (see above). The theme could be stored anywhere in the request context like a session variable, cookie, or user profile settings.

The filter will extend the potential locations for views by prefixing a Themes folder and the name of the theme to the potential locations. For example, with Theme:CleanBlue, Controller:Foo, and View:Bar the potential locations:

  • Foo/Bar.spark
  • Shared/Bar.spark

become:

  • Themes/CleanBlue/Foo/Bar.spark
  • Themes/CleanBlue/Sales/Shared/Bar.spark
  • Foo/Bar.spark
  • Shared/Bar.spark

LanguageDescriptorFilter

Like the theme filter, the language descriptor filter requires a lambda expression when it's registered.

This filter operates by inserting the language-locale, and just the language, before the extension on each potential location. For example, with Langauge:en-US, Controller:Foo, and View:Bar the potential locations:

  • Foo/Bar.spark
  • Shared/Bar.spark

become:

  • Foo/Bar.en-US.spark
  • Foo/Bar.en.spark
  • Foo/Bar.spark
  • Shared/Bar.en-US.spark
  • Shared/Bar.en.spark
  • Shared/Bar.spark

Custom IDescriptorFilter implementations

It should be very simple to implement a custom descriptor filter. The only two methods are ExtraParameters, which is called frequently to extract significant values for the filter from the current context, and PotentialLocations, which is called on a cache-miss to extend the list of paths where a given view template may exist.

As an example, this is the implementation of the AreaDescriptorFilter.

public class AreaDescriptorFilter : DescriptorFilterBase
{
    public override void ExtraParameters(
        ControllerContext context, 
        IDictionary<string, object> extra)
    {
        object value;
        if (context.RouteData.Values.TryGetValue("area", out value))
            extra["area"] = value;
    }
 
    public override IEnumerable<string> PotentialLocations(
        IEnumerable<string> locations, 
        IDictionary<string, object> extra)
    {
        string areaName;
        return TryGetString(extra, "area", out areaName)
                   ? locations.Select(x => Path.Combine(areaName, x)).Concat(locations)
                   : locations;
    }
}

Understanding IViewFolder and IViewFile

The Spark library declares two interfaces which provide the ability to locate view files and load their contents. There are very few methods and several implementations of typical view locations are provided, so in most cases the most you will need to do is provide additional configuration.

Default view folder locations

In a Castle MonoRail application the configured IViewSourceLoader is used by the default IViewFolder implementation. The root location of the default IViewSourceLoader is ~/Views and can be changed with the monorail/viewEngines/@viewPathRoot configuration attribute.

In an Asp.Net MVC applications a view folder based on hosting environment's VirtualPathProvider is used. The root location within this will be ~/Views.

The behavior of those abstractions can be controlled in ways that aren't documented here; custom VirtualPathProviders is a large topic all on it's own. Whatever you do to the framework to alter how files are loaded should work with Spark as expected.

What is documented on this page is how to bring in additional IViewFolder implementations to extend the way spark files can be located.

Adding a view folder to config

The <spark> config section accepts a <views> element which can be used to add additional view folder locations.

<spark>
  <views>
    <add name="{any-unique-name}" 
        folderType="FileSystem|EmbeddedResource|VirtualPathProvider|Custom"
        type="{name, assembly of IViewFolder type}"
        constuctor-param-names="values"
        subfolder="{optional subfolder to target}"/>
  </views>
</spark>

For example:

<spark>
  <views>
    <add name="stuff" folderType="EmbeddedResource" assembly="MyExtraStuff" resourcePath="MyExtraStuff.Views"/>
  </views>
</spark>

Understanding the subfolder parameter

The subfolder parameter will make the contents for a view folder appear at a specific location in the logical views directory. In other words you're adding files, but you're adding them at a subdirectory.

For example if you create a ~/Masters directory in your web app you could expose it in place with the following:

<spark>
  <views>
    <add name="morestuff" folderType="VirtualPathProvider" subfolder="layouts" virtualBaseDir="~/Masters"/>
  </views>
</spark>

Any path can be the target for adding additional files. If no subfolder is provided the contents are added at the root.