Manage EPiServer global settings in multilanguage multisite environment

Previously I wrote an article about an Easy way to manage global settings in multilanguage EPiServer website, but have since then had the need to expand this functionality a little (If you feel this article lacks code, check the old one to get it). Together with my collegue Thomas Durrani from Atiendo Consulting I refactored the existing code to also work in a multisite EPiServer installation.

Adding EPiServer multisite support to global settings functionality

First off, the GlobalSettings class which previously contained all of the code have now been reduced to almost nothing. This is due to our need of providing an easy way of mocking the settings for our unit tests.

GlobalSettings.cs

public class GlobalSettings
{ 
  public static SettingsPage Instance
  {
    get
    {
      return ServiceLocator.Current.GetInstance<ISettingsPageRepository>().SettingsPageForCurrentCulture();
    }
  }
}

As you can see, the Instance property is still static, so you will be able to access it in the same way as before.

var setting = GlobalSettings.Instance.IntegerSetting;

However, all the functionality has been moved to a SettingsPageRepository. The interface used to fetch the instance exposes a method for retrieving the settings page for the culture found in the current context.

ISettingsPageRepository.cs

public interface ISettingsPageRepository
{
  SettingsPage SettingsPageForCurrentCulture();
  SettingsPage SettingsPageFor(CultureInfo cultureInfo);
}

The interface also exposes a method in which you can specify for which culture you would like the settings page. This is useful while for instance writing EPiServer Scheduled Jobs where you will have no HttpContext while they are running on schedule, or in any other case where you can’t get the current culture automatically.

SettingsPageRepository.cs

public class SettingsPageRepository : ISettingsPageRepository
{
  private readonly IContentRepository _contentRepository;
  private readonly IContentTypeRepository _contentTypeRepository;
  private readonly IContentLanguageService _contentLanguageService;
  private readonly ICache<ContentReference> _cache;

In order to keep this short I’ve omitted the constructor for this class. All of the above properties are injected through constructor injection using StructureMap. If you’re curious about the implementation of the ICache interface, what we’re using is similar to the one described in this article: Cache manager easing cache handling for EPiServer 7.5 with ISynchronizedObjectInstanceCache.

public SettingsPage SettingsPageForCurrentCulture()
{
  var culture = _contentLanguageService.PreferredCulture;
  return GetOrCreateSettingsPageFor(culture);
}

public SettingsPage SettingsPageFor(CultureInfo culture)
{
   return GetOrCreateSettingsPageFor(culture);
}

private SettingsPage GetOrCreateSettingsPageFor(CultureInfo culture)
{
   var key = string.Format("SettingsPageFor_{0}", culture.Name.ToLower());
   var contentLink = _cache.Get(key, () => GetOrCreate<SettingsPage>(ParentPage, DefaultPageName, culture));
   return _contentRepository.Get<SettingsPage>(contentLink, culture);
}

The two public methods both use a third private one to retrieve the SettingsPage object, one just passing on the recieved culture, and the other trying to determine the culture itself by requesting it from the ContentLanguageService. The SettingsPage itself is not cached since EPiServer does a good job with this, however since all languages in the EPiServer installation uses the same page (just different language versions), we can optimize a bit by caching the ContentReference.

private ContentReference GetOrCreate<TContentType>(ContentReference parentReference, string name, CultureInfo language)
where TContentType : IContentData
{
  var page = _contentRepository
         .GetChildren<TContentType>(parentReference, language)
         .FirstOrDefault(x => ((IContent)x).Name == name);

  if (page != null)
  {
    var existingReference = ((IContent)page).ContentLink;
    return existingReference;
  }

  var masterPage = GetMasterPage<SettingsPage>(parentReference, name);
  if (language.Name == Constants.Languages.Swedish)
  {
    return masterPage;
  }

  var newLanguageBranch = _contentRepository.CreateLanguageBranch<SettingsPage>(masterPage.ToPageReference(), new LanguageSelector(language.Name));

  var content = (IContent) newLanguageBranch;
  content.Name = name;
  var reference = _contentRepository.Save(content, SaveAction.Publish, AccessLevel.NoAccess);
  return reference;
}

This is roughly the same as in the previous article, with the difference that we now work a little more with the language versions. If we don’t have a settings page for the current language, one is created automatically.

private ContentReference GetMasterPage<TContentType>(ContentReference parentReference, string name)
  where TContentType : IContentData
{
  var masterCi = CultureInfo.GetCultureInfo(Constants.Languages.Swedish);
  var page = _contentRepository
      .GetChildren<TContentType>(parentReference, masterCi)
      .FirstOrDefault(x => ((IContent)x).Name == name);

  if (page != null)
  {
    var existingReference = ((IContent)page).ContentLink;
    return existingReference;
  }

  var clone = _contentRepository.GetDefault<TContentType>(parentReference, masterCi);
  var content = ((IContent)clone);
  content.Name = name;
  var reference = _contentRepository.Save(content, SaveAction.Publish, AccessLevel.NoAccess);
  return reference;
}

The masterpage get method is also rather straight forward. It assumes that the master language is Swedish. Also the ParentPage is the root page just as before, and the default page name is [GlobalSettings].

The SettingsPage class also get some alterations. We’ve now added a separate block for each site (88-98), and keep common properties that should be the same for all sites separately.

SettingsPage.cs

[Display(
  Name = "A common settings",
  GroupName = Constants.ContentTypes.Tabs.CommonSettings,
  Order = 150)]
[CultureSpecific]
public virtual int ACommonIntegerSetting  { get; set; }
		
[Display(
  Name = "Site-1 Settings",
  GroupName = Constants.ContentTypes.Tabs.Site1Settings,
  Order = 500)]
public virtual Site1Block Site1 { get; set; }

[Display(
  Name = "Site-2 Settings",
  GroupName = Constants.ContentTypes.Tabs.Site2Settings,
  Order = 600)]
public virtual Site2Block Site2 { get; set; }
		
public ISiteSettings CurrentSite
{
  get
  {
    var startPage =  ServiceLocator.Current.GetInstance<IContentLoader>()
          .Get<StartPageBase>(ContentReference.StartPage);
    if (startPage is Site1StartPage)
    {
      return Site1;
    }
    return Site2;
  }
}

The final thing added is a current site property. This will automatically give you the current settings, without you having to worry about which website you’re on in your EPiServer installation. I’m sure you can think of a more clever way of determining which site you’re on, but this was good enough for our purposes.

var setting = GlobalSettings.Instance.CurrentSite.SomeSiteSpecificSetting;

Both the site specific blocks need to implement the ISiteSettings interface, and it need to contain all the properties that all of the sites use, regretfully. Also you’ll need to ignore the ones you don’t need in your block implementations in order to keep EPiServer from synchronizing them to the database.