August 25, 2011

Designing a theme api - configuration

A week or so ago I blogged about the new theme system I developed for fabrik. The post covered some of the technical aspects of developing such a system, in particular, how we can build a generic interface based on a theme configuration.

What I didn’t mention in the post was that I didn’t have a way of persisting the theme definitions. We actually put this feature into production a week ago with the theme definitions hardcoded. Obviously we need a better way of storing the theme definitions so that we can add new themes easily, or update existing themes when required.

Based on the hierarchical structure of a theme definition (with options, layouts and presets) the logical choice for storage was Xml.

Rather than starting from scratch I had a dig around the source code for NuGet to see how they handle the NuGet specification files. This saved me a bunch of time, especially when it came to validating the theme definitions.

The following example shows a complete theme definition. I created a XSD that I validate the definitions against. It also gives the theme developer intellisense in Visual Studio.

<?xml version="1.0" encoding="utf-8" ?>
<theme xmlns="http://tempuri.org/themespec.xsd">
  <metadata>
    <id>testtheme</id>
    <version>1.0</version>
    <title>Test Theme</title>
    <description>This is a test theme</description>
    <authors>fabrik</authors>
    <supportUrl>http://www.onfabrik.com</supportUrl>
    <imageUrl>http://www.onfabrik.com/themes/testtheme/testtheme.png</imageUrl>
    <requiresSubscription>true</requiresSubscription>

    <themeOptions>
      <themeOption name="BackgroundColor" 
                   displayName="Background Color" 
                   dataType="System.String" 
                   defaultValue="ffffff" 
                   description="The background color for your site"/>
      
      <themeOption name="HomePageLayout" 
                   displayName="Home Page Layout" 
                   dataType="System.String" 
                   defaultValue="Layout1" 
                   options="_HomePage1, _HomePage2" 
                   template="LayoutSelector"/>
    </themeOptions>

    <themeLayouts>
      <themeLayout systemName="_HomePage1" 
                   title="Home Page 1" 
                   description="Home Page Layout 1" 
                   imageUrl="http://www.onfabrik.com/themes/testtheme/homepage1.png"/>
      
      <themeLayout systemName="_HomePage2"
                   title="Home Page 2"
                   description="Home Page Layout 2"
                   imageUrl="http://www.onfabrik.com/themes/testtheme/homepage2.png"/>
    </themeLayouts>

    <themePresets>
      <themePreset name="Default" colors="ffffff,333333,ff9933">
        <values>
          <add key="BackgroundColor" value="ffffff"/>
          <add key="HomePageLayout" value="_HomePage1"/>
        </values>
      </themePreset>
    </themePresets>
    
  </metadata>
</theme>

The plan is to upload the theme definitions to Azure Blob storage. This makes it easy to make changes without needing to redeploy the application.

The following code shows how we load a theme from a theme definition file.

var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "testtheme.xml");
var themeManifest = ThemeManifest.ReadFrom(File.OpenRead(configPath));
var theme = new Theme(themeManifest.Metadata);

ThemeManifest is a special class responsible for parsing and validating the theme definition file. It also contains methods for generating theme definitions. ThemeManifest has a “ThemeManifestMetadata” object that contains all the necessary Xml attributes for us to deserialize the theme definition using XmlSerializer.

Once we have the theme manifest we can create a theme. The Theme class is what is exposed to the rest of the application.

As you can see from the xml file above, each theme option requires a default value. This is so that we can handle differences between the theme definition and a user’s theme configuration. Basically if we add a new theme option then the user will get the default value until they go to the customization page and make a change.

The process is quite simple. First we create a theme configuration using the default values of the theme. Then we check the stored user values (essentially just key value pairs) and override the default values where appropriate.

public ThemeConfiguration BuildConfiguration(Guid siteId, IDictionary<string, string> optionValues) {
        
    if (optionValues == null)
        throw new ArgumentNullException("optionValues");

    var config = BuildConfiguration(siteId); // loads defaults

    foreach (var option in theme.ThemeOptions)
    {
        if (optionValues.ContainsKey(option.Name))
        {
            config.Options[option.Name] = 
                GetOptionValue(option, optionValues[option.Name]);
        }
    }

    return config;
}

The “GetOptionValue” method does the job of converting the stored string value into the correct type. This is necessary in order for asp.net mvc to automatically generate the correct inputs on our customization page.

private static object GetOptionValue(ThemeOption option, string optionValue) {

    if (option == null) {
        throw new ArgumentException("option");
    }

    if (string.IsNullOrEmpty(optionValue))
        return option.DefaultValue;
        
    var converter = TypeDescriptor.GetConverter(option.DataType);

    return converter.IsValid(optionValue) 
        ? converter.ConvertFromString(optionValue) 
        : option.DefaultValue;
}

The following code shows how we build the configuration. Note that we have one valid theme option and one invalid one. ThemeConfigurationBuilder handles this gracefully.

var optionValues = new Dictionary<string, string> { 
	{ "BackgroundColor", "000000" }, 
	{ "Foo", "Bar" } 
};

var config = 
	new ThemeConfigurationBuilder(theme)
		.BuildConfiguration(Guid.NewGuid(), optionValues);

You can get the full source code on bitbucket. Let me know what you think.

© 2022 Ben Foster