Thoughts from the Wet Coast

The musings of an ASP.NET Developer from Canada's We(s)t Coast

Comments

Articles

 
     

Naif.Blog: 3. Adding Theming

Category: ASP.NET Core
May 23 2016

In my continuing series on building my own Blog Application I next turn to theming.  Any self-respecting Blog Application needs to be able to be themed and this is actually fairly straightforward in ASP.NET Core.  In addition to introducing the theming engine this blog will also introduce the new Configuration and Options frameworks available in ASP.NET Core.

ASP.NET Configuration

The first step in building a theming engine is providing a mechanism for the user to specify the them they want to use.  To do this I am going to use the new ASP.NET Configuration functionality.

Configuration in ASP.NET 4.5 is through the use of the xml based web.config file.  ASP.NET Core, however, has a much more flexible configuration system.  This is exemplified by the Startup class constructor.

public Startup(IHostingEnvironment env)
{
    // Set up configuration providers.
    var builder = new ConfigurationBuilder()
        .AddJsonFile("appsettings.json")
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

    builder.AddEnvironmentVariables();
    Configuration = builder.Build();
}

In this snippet a new ConfigurationBuilder is created and two separate JSON files are added.  In the next line of code Environment Variables are added.  This demonstrates the ability to define configuration in many different ways and combine various configuration sources into a single Configuration object.

ASP.NET Options

But the flexibility doesn’t stop with the ability to combine multiple sources of configuration.  ASP.NET Core provides the ability to access your configuration settings through the use of strongly-typed Options.

We can add support for ASP.NET Options by calling the AddOptions method in the Startup class ConfigureServices method.  Then we can call the Configure method to convert a configuration section into a strongly-typed object.

public void ConfigureServices(IServiceCollection services)
{
    services.AddCaching();
    services.AddTransient<IBlogRepository, XmlBlogRepository>();

    services.AddOptions();

    services.Configure<BlogOptions>(Configuration.GetSection("BlogOptions"));

    services.AddMvc();
}

The Configure method works by matching the setting name to a property name.  In our BlogOptions example, the appsettings.json looks like:

{
  "BlogOptions" : {
    "Theme": "Theme1"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Verbose",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

while the BlogOptions class looks like:

public class BlogOptions
{
    public string Theme { get; set; }
}

So why do we go to the effort of using the Options framework?  There are two reasons.  The first is that the options are available to any class that supports ASP.NET Core’s built-in Dependency Injection, by adding a property of type IOptions<BlogOptions>, and the second reason is that the options are exposed as strongly-typed objects.

private BlogOptions _options;

public ExampleController(IOptions<BlogOptions> optionsAccessor)
{
    _options = optionsAccessor.Value;
}

So we have completed the first step of building a theming engine for our Blog Application, by providing the Blog user with the ability to define the Theme.

View Location Expanders

ASP.NET Core has a flexible method of identifying where to look for the correct View(s).  The RazorViewEngine uses a collection of View Location Expanders.  A View Location Expander must implement the IViewLocationExpander interface to return a list of possible path templates for the Engine to Search.  The default View Location Expander returns the following list of paths

  • /Views/{1}/{0}.cshtml
  • /Views/Shared/{1}/{0}.cshtml

The theming setup that I would like to use is to have a “theme” folder immediately below the Views folder.  The folder structure below the “theme” folder will be the same as the default folder structure.  In addition, if no View is provided in a particular theme then I would like to return the View from the default folder.  These business rules imply that I need to return two additional paths.

  • /Views/{theme}/{1}/{0}.cshtml
  • /Views/{theme}/Shared/{1}/{0}.cshtml

So lets build our custom View Location Expander.  The IViewLocationExpander interface has two methods – PopulateValues and ExpandViewLocations.  In our scenario, PopulateValues is used to retrieve the user selected them from the BlogOptions class and save it to the ViewLocationExpanderContext.

public void PopulateValues(ViewLocationExpanderContext context)
{
    var optionsAccessor = context.ActionContext.HttpContext.RequestServices
                .GetService(typeof(IOptions<BlogOptions>)) as IOptions<BlogOptions>;
    var options = optionsAccessor.Value;

    if (!string.IsNullOrEmpty(options.Theme))
    {
        context.Values["theme"] = options.Theme;
    }
}

The ExpandViewLocations method is then used to modify the View Locations to search.  It takes as a parameter the current list of View Locations and returns the modified list.  Using this pattern we can either modify the existing list or completely replace it.  As we want to be able to fall back to the default list of view paths if we cannot find the View in our theme we will add our new paths to the list, and because we want our new paths to be searched first we will insert them before the existing list, as shown below.

public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
    var themeLocations = viewLocations.ToList();
    if (context.Values.ContainsKey("theme"))
    {
        themeLocations.InsertRange(0, viewLocations.Select(f => {
            return f.Replace("/Views/", "/Views/" + context.Values["theme"] + "/");
        }));
    }
    return themeLocations;
}

If we have defined a theme in our appsettings.json file then the paths returned will be

  • /Views/{theme}/{1}/{0}.cshtml
  • /Views/{theme}/Shared/{1}/{0}.cshtml
  • /Views/{1}/{0}.cshtml
  • /Views/Shared/{1}/{0}.cshtml

which provides the necessary fallback.

In order to use the new View Location Expander we need to update the ConfigureServices method in Startup as follows.

public void ConfigureServices(IServiceCollection services)
{
    services.AddCaching();
    services.AddTransient<IBlogRepository, XmlBlogRepository>();

    services.AddOptions();

    services.Configure<BlogOptions>(Configuration.GetSection("BlogOptions"));

    services.AddMvc();

    services.Configure<RazorViewEngineOptions>(options =>
    {
        options.ViewLocationExpanders.Add(new ThemeViewLocationExpander());
    });
}

As with each of my posts on building the Blog Application I have tagged a release that matches the source used in this post.  I have included two very simple themes that demonstrate the features of the theming engine – Theme1 provides both custom “_Layout” and “Index” views, while Theme2 provides just a _Layout view, so using this them will render the default Index view.

Source

The source for Naif.Blog can be found at https://github.com/cnurse/Naif.Blog.  If you want to find the state of the repository used in this post then you can find that at the tagged release v0.0.3 (https://github.com/cnurse/Naif.Blog/releases/tag/v0.0.3)

Categories

Tags