Theme-amajig Refactored: Using Feature Properties

In a previous post, I described a feature that would take install and retract modifications to SPTHEMES.XML. Peter Seale suggested providing a method to reapply the changes without a deactivate/activate cycle, specifically for new servers added to a farm. It should be as simple as providing a user interface to call FeatureThemesJob.InstallThemes, but that presents a bit of a problem: InstallThemes expects the name of the themes file, which I declare in the feature receiver. So before we can work on a reapplication interface, let’s move that file name to a more accessible location.

The Revised Feature

A better way to store the theme file name would be as a Feature Property:

<Feature
  Id="E2F8D046-607D-4BB6-93CC-2C04CF04099E"
  Title="SPHOLS Themes"
  Description="Installs SPHOLS and SPHOLSX themes on farm."
  Version="1.0.0.0"
  Scope="Farm"
  ReceiverAssembly="MyBranding, ..."
  ReceiverClass="MyBranding.MyThemesFeatureReceiver"
  xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementFile Location="SPTHEMES.XML" />
  </ElementManifests>
  <Properties>
    <Property Key="Solutionizing:ThemesFile" Value="SPTHEMES.XML" />
  </Properties>
</Feature>

And we can remove references to THEMES_FILE from our receiver:

namespace MyBranding {
  public class MyThemesFeatureReceiver : SPFeatureReceiver {
    public override void FeatureActivated(SPFeatureReceiverProperties properties) {
      if (properties == null)
        throw new ArgumentNullException("properties");
      FeatureThemesJob.InstallThemes(properties.Definition);
    }

    public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {
      if (properties == null)
        throw new ArgumentNullException("properties");
      FeatureThemesJob.DeleteThemes(properties.Definition);
    }

    public override void FeatureInstalled(SPFeatureReceiverProperties properties) { }
    public override void FeatureUninstalling(SPFeatureReceiverProperties properties) { }
  }
}

Note that this code is now completely feature-agnostic, reusable for any themes feature.

FeatureThemesJob

Other than purging the logic to persist _themesFile, which I’ll leave as an exercise for the reader, we just need to update Execute to use our new feature property:

    private const string PROP_THEMES_FILE = "Solutionizing:ThemesFile"
    private const string SPTHEMES_PATH = @"TEMPLATE\LAYOUTS\1033\SPTHEMES.XML";
    public override void Execute(Guid targetInstanceId) {
      SPFeatureDefinition fDef = Farm.FeatureDefinitions[_featureID];
      if (fDef != null) {
        SPFeatureProperty themesFileProp = fDef.Properties[PROP_THEMES_FILE];
        if(themesFileProp == null)
          throw new SPException(string.Format("Feature '{0}' is missing property '{1}'.", fDef.DisplayName, PROP_THEMES_FILE));

        DoMerge(SPUtility.GetGenericSetupPath(SPTHEMES_PATH), Path.Combine(fDef.RootDirectory, themesFileProp.Value));
      }
    }

But since we’re in a refactoring mood, we might as well extract the code to retrieve the themes file path:

    internal const string PROP_THEMES_FILE = "Solutionizing:ThemesFile"
    private const string ERR_FEATURE_NOT_FOUND = "Feature '{0}' not found in farm.";
    private const string ERR_MISSING_PROPERTY = "Feature '{0}' is missing property '{1}'.";
    internal string ThemesFilePath {
      get {
        SPFeatureDefinition fDef = Farm.FeatureDefinitions[_featureID];
        if (fDef == null)
          throw new SPException(string.Format(ERR_FEATURE_NOT_FOUND, _featureID));

        SPFeatureProperty prop = fDef.Properties[PROP_THEMES_FILE];
        if (prop == null)
          throw new SPException(string.Format(ERR_MISSING_PROPERTY, fDef.DisplayName, PROP_THEMES_FILE));

        return Path.Combine(fDef.RootDirectory, prop.Value);
      }
    }

Which makes Execute rather elegant:

    private const string SPTHEMES_PATH = @"TEMPLATE\LAYOUTS\1033\SPTHEMES.XML";
    public override void Execute(Guid targetInstanceId) {
      DoMerge(SPUtility.GetGenericSetupPath(SPTHEMES_PATH), ThemesFilePath);
    }

Now everything we need for the timer job is available from the feature definition. The next step is to build an interface to run the job on demand. Stay tuned!