Settings Tab Implementation to handle different master languages between multiple sites

Settings Tab Implementation to handle different master languages between multiple sites

Following several iterations of the settings tab implementation, that you can look up in this blog post series: language support, balanced environment support and a simpler version, we encountered a not so common scenario when you have two sites with the first site having as master language any language and the second site having as master language any other language different to the first site. In this scenario, the last 3 iterations were not able to handle correctly the scenario because in the initialization method UpdateSettings() we were not taking into account the default language of the items inside the site settings folder and for this scenario, the second site does not initialize any setting base item. Please pay special attention to the comments in the code below.

        public void UpdateSettings()
        {
            var root = _contentRepository.GetItems(_contentRootService.List(), new LoaderOptions())
                 .FirstOrDefault(x => x.ContentGuid == SettingsFolder.SettingsRootGuid);
 
            if (root == null)
            {
                return;
            }
 
            GlobalSettingsRoot = root.ContentLink;
            var children = _contentRepository.GetChildren<SettingsFolder>(GlobalSettingsRoot).ToList();
            foreach (var site in _siteDefinitionRepository.List())
            {
                var folder = children.Find(x => x.Name.Equals(site.Name, StringComparison.InvariantCultureIgnoreCase));

                // Up to this points everything works as expected
                if (folder != null)
                {
                    // Children items here are not returned correctly for the second site with different master language
                    foreach (var child in _contentRepository.GetChildren<SettingsBase>(folder.ContentLink))
                    {
                        UpdateSettings(site.Id, child);
                    }
                    continue;
                }
                CreateSiteFolder(site);
            }
        }

To solve it, we modified slightly this function by getting first the default language for the current site based on the folder name, which should be the same as the site name, and get the children site setting base items using this language. In order to do this, we added two functions to get languages which we already kind of used in the first blog of the series for language support. The first function is called Get Available languages and it gets all enabled languages in the CMS.

        private List<LanguageBranch> GetAvailableLanguages()
        {
            return _languageBranchRepository.ListEnabled()?.ToList() ?? new List<LanguageBranch>();
        }

The second function is called Get Current Language By Site which receives a site definition parameter which then tries to get the start page reference for the site in order to filter the master branch from the list of available languages in the CMS. If in the worst case scenario no language is found, we return the first encountered language in the list of available languages as default.

        private LanguageBranch GetCurrentLanguageBySite(SiteDefinition site)
        {
            var startPage = site.StartPage?.Get<PageData>();
            var availableLanguages = GetAvailableLanguages();
            var defaultLanguage = availableLanguages.FirstOrDefault();

            // If language is not detected properly by the current thread property use the content language preferred culture instead
            var currentLanguage = availableLanguages.Find(x => x.LanguageID == startPage?.LanguageBranch());
            return currentLanguage ?? defaultLanguage;
        }

The final implementation of the new Update Settings function is the following:

       public void UpdateSettings()
        {
            var root = _contentRepository.GetItems(_contentRootService.List(), new LoaderOptions())
                 .FirstOrDefault(x => x.ContentGuid == SettingsFolder.SettingsRootGuid);

            if (root == null)
            {
                return;
            }

            GlobalSettingsRoot = root.ContentLink;
            var children = _contentRepository.GetChildren<SettingsFolder>(GlobalSettingsRoot).ToList();
            foreach (var site in _siteDefinitionRepository.List())
            {
                var folder = children.Find(x => x.Name.Equals(site.Name, StringComparison.InvariantCultureIgnoreCase));
                // Get master language for the current site
                var defaultLanguage = GetCurrentLanguageBySite(site);
                if (folder != null)
                {
                    // Get setting base items using the default language found
                    var siteSettings = _contentRepository.GetChildren<SettingsBase>(folder.ContentLink, defaultLanguage.Culture);
                    foreach (var siteSetting in siteSettings)
                    {
                        UpdateSettings(site.Id, siteSetting);
                    }
                    continue;
                }
                CreateSiteFolder(site);
            }
        }

And that is all, with this implementation even if you have different master languages among multiple sites you will be able to use the site settings tab feature without issues. The final code for the Setting Service class now look like this:

using EPiServer;
using EPiServer.Cms.Shell;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAccess;
using EPiServer.Events;
using EPiServer.Events.Clients;
using EPiServer.Framework.TypeScanner;
using EPiServer.Logging;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using Foundation.Cms.Extensions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Foundation.Cms.Settings
{
    public class SettingsService : ISettingsService
    {
        //Generate unique id for your event and the raiser
        private readonly Guid _raiserId;
        private static Guid EventId => new Guid("888B5C89-9B0F-4E67-A3B0-6E660AB9A60F");


        private readonly IContentRepository _contentRepository;
        private readonly ContentRootService _contentRootService;
        private readonly IContentTypeRepository _contentTypeRepository;
        private readonly ILogger _log = LogManager.GetLogger();
        private readonly ITypeScannerLookup _typeScannerLookup;
        private readonly IContentEvents _contentEvents;
        private readonly ISiteDefinitionEvents _siteDefinitionEvents;
        private readonly ISiteDefinitionRepository _siteDefinitionRepository;
        private readonly ISiteDefinitionResolver _siteDefinitionResolver;
        private readonly ServiceAccessor<HttpContextBase> _httpContext;
        private readonly IEventRegistry _eventRegistry;
        private readonly ILanguageBranchRepository _languageBranchRepository;

        public SettingsService(
            IContentRepository contentRepository,
            ContentRootService contentRootService,
            ITypeScannerLookup typeScannerLookup,
            IContentTypeRepository contentTypeRepository,
            IContentEvents contentEvents,
            ISiteDefinitionEvents siteDefinitionEvents,
            ISiteDefinitionRepository siteDefinitionRepository,
            ISiteDefinitionResolver siteDefinitionResolver,
            ServiceAccessor<HttpContextBase> httpContext,
            IEventRegistry eventRegistry,
            ILanguageBranchRepository languageBranchRepository)
        {
            _contentRepository = contentRepository;
            _contentRootService = contentRootService;
            _typeScannerLookup = typeScannerLookup;
            _contentTypeRepository = contentTypeRepository;
            _contentEvents = contentEvents;
            _siteDefinitionEvents = siteDefinitionEvents;
            _siteDefinitionRepository = siteDefinitionRepository;
            _siteDefinitionResolver = siteDefinitionResolver;
            _httpContext = httpContext;
            _eventRegistry = eventRegistry;
            _languageBranchRepository = languageBranchRepository;

            _raiserId = Guid.NewGuid();
        }

        public ConcurrentDictionary<Guid, Dictionary<Type, Guid>> SiteSettings { get; } = new ConcurrentDictionary<Guid, Dictionary<Type, Guid>>();

        public ContentReference GlobalSettingsRoot { get; set; }

        public List<T> GetAllSiteSettings<T>() where T : SettingsBase
        {
            var sites = _siteDefinitionRepository.List();
            var siteSettings = new List<T>();

            foreach (var site in sites)
            {
                siteSettings.Add(GetSiteSettings<T>(site.Id));
            }

            return siteSettings;
        }

        public T GetSiteSettings<T>(Guid? siteId = null) where T : SettingsBase
        {
            if (!siteId.HasValue)
            {
                siteId = ResolveSiteId();
                if (siteId == Guid.Empty)
                {
                    return default;
                }
            }

            try
            {
                if (SiteSettings.TryGetValue(siteId.Value, out var siteSettings) &&
                    siteSettings.TryGetValue(typeof(T), out var settingId))
                {
                    return _contentRepository.Get<T>(settingId);
                }
            }
            catch (KeyNotFoundException keyNotFoundException)
            {
                _log.Error($"[Settings] {keyNotFoundException.Message}", exception: keyNotFoundException);
            }
            catch (ArgumentNullException argumentNullException)
            {
                _log.Error($"[Settings] {argumentNullException.Message}", exception: argumentNullException);
            }

            return default;
        }

        public void InitializeSettings()
        {
            try
            {
                RegisterContentRoots();
            }
            catch (NotSupportedException notSupportedException)
            {
                _log.Error($"[Settings] {notSupportedException.Message}", exception: notSupportedException);
                throw;
            }

            _contentEvents.PublishedContent += PublishedContent;
            _siteDefinitionEvents.SiteCreated += SiteCreated;
            _siteDefinitionEvents.SiteUpdated += SiteUpdated;
            _siteDefinitionEvents.SiteDeleted += SiteDeleted;

            var settingsEvent = _eventRegistry.Get(EventId);
            settingsEvent.Raised += SettingsEvent_Raised;
        }

        public void UnInitializeSettings()
        {
            _contentEvents.PublishedContent -= PublishedContent;
            _siteDefinitionEvents.SiteCreated -= SiteCreated;
            _siteDefinitionEvents.SiteUpdated -= SiteUpdated;
            _siteDefinitionEvents.SiteDeleted -= SiteDeleted;

            var settingsEvent = _eventRegistry.Get(EventId);
            settingsEvent.Raised -= SettingsEvent_Raised;
        }

        public void UpdateSettings(Guid siteId, IContent content)
        {
            var contentType = content.GetOriginalType();
            try
            {
                if (!SiteSettings.ContainsKey(siteId))
                {
                    SiteSettings[siteId] = new Dictionary<Type, Guid>();
                }

                SiteSettings[siteId][contentType] = content.ContentGuid;
            }
            catch (KeyNotFoundException keyNotFoundException)
            {
                _log.Error($"[Settings] {keyNotFoundException.Message}", exception: keyNotFoundException);
            }
            catch (ArgumentNullException argumentNullException)
            {
                _log.Error($"[Settings] {argumentNullException.Message}", exception: argumentNullException);
            }
        }

        public void UpdateSettings()
        {
            var root = _contentRepository.GetItems(_contentRootService.List(), new LoaderOptions())
                 .FirstOrDefault(x => x.ContentGuid == SettingsFolder.SettingsRootGuid);

            if (root == null)
            {
                return;
            }

            GlobalSettingsRoot = root.ContentLink;
            var children = _contentRepository.GetChildren<SettingsFolder>(GlobalSettingsRoot).ToList();
            foreach (var site in _siteDefinitionRepository.List())
            {
                var folder = children.Find(x => x.Name.Equals(site.Name, StringComparison.InvariantCultureIgnoreCase));
                var defaultLanguage = GetCurrentLanguageBySite(site);
                if (folder != null)
                {
                    var siteSettings = _contentRepository.GetChildren<SettingsBase>(folder.ContentLink, defaultLanguage.Culture);
                    foreach (var siteSetting in siteSettings)
                    {
                        UpdateSettings(site.Id, siteSetting);
                    }
                    continue;
                }
                CreateSiteFolder(site);
            }
        }

        private List<LanguageBranch> GetAvailableLanguages()
        {
            return _languageBranchRepository.ListEnabled()?.ToList() ?? new List<LanguageBranch>();
        }

        private LanguageBranch GetCurrentLanguageBySite(SiteDefinition site)
        {
            var startPage = site.StartPage?.Get<PageData>();
            var availableLanguages = GetAvailableLanguages();
            var defaultLanguage = availableLanguages.FirstOrDefault();

            // If language is not detected properly by the current thread property use the content language preferred culture instead
            var currentLanguage = availableLanguages.Find(x => x.LanguageID == startPage?.LanguageBranch());
            return currentLanguage ?? defaultLanguage;
        }

        private void RegisterContentRoots()
        {
            var registeredRoots = _contentRepository.GetItems(_contentRootService.List(), new LoaderOptions());
            var settingsRootRegistered = registeredRoots.Any(x => x.ContentGuid == SettingsFolder.SettingsRootGuid && x.Name.Equals(SettingsFolder.SettingsRootName));

            if (!settingsRootRegistered)
            {
                _contentRootService.Register<SettingsFolder>(SettingsFolder.SettingsRootName, SettingsFolder.SettingsRootGuid, ContentReference.RootPage);
            }

            UpdateSettings();
        }

        private void CreateSiteFolder(SiteDefinition siteDefinition)
        {
            var folder = _contentRepository.GetDefault<SettingsFolder>(GlobalSettingsRoot);
            folder.Name = siteDefinition.Name;
            var reference = _contentRepository.Save(folder, SaveAction.Publish, AccessLevel.NoAccess);

            var settingsModelTypes = _typeScannerLookup.AllTypes
                .Where(t => t.GetCustomAttributes(typeof(SettingsContentTypeAttribute), false).Length > 0);

            foreach (var settingsType in settingsModelTypes)
            {
                if (!(settingsType.GetCustomAttributes(typeof(SettingsContentTypeAttribute), false)
                    .FirstOrDefault() is SettingsContentTypeAttribute attribute))
                {
                    continue;
                }

                var contentType = _contentTypeRepository.Load(settingsType);
                var newSettings = _contentRepository.GetDefault<IContent>(reference, contentType.ID);
                newSettings.Name = attribute.SettingsName;

                try
                {
                    _contentRepository.Save(newSettings, SaveAction.Publish, AccessLevel.NoAccess);
                    UpdateSettings(siteDefinition.Id, newSettings);
                }
                catch (Exception e)
                {
                    _log.Error(e.Message);
                }
            }
        }

        private void SiteCreated(object sender, SiteDefinitionEventArgs e)
        {
            if (_contentRepository.GetChildren<SettingsFolder>(GlobalSettingsRoot)
                .Any(x => x.Name.Equals(e.Site.Name, StringComparison.InvariantCultureIgnoreCase)))
            {
                return;
            }

            CreateSiteFolder(e.Site);
        }

        private void SiteDeleted(object sender, SiteDefinitionEventArgs e)
        {
            var folder = _contentRepository.GetChildren<SettingsFolder>(GlobalSettingsRoot)
                .FirstOrDefault(x => x.Name.Equals(e.Site.Name, StringComparison.InvariantCultureIgnoreCase));

            if (folder == null)
            {
                return;
            }

            _contentRepository.Delete(folder.ContentLink, true, AccessLevel.NoAccess);
        }

        private void SiteUpdated(object sender, SiteDefinitionEventArgs e)
        {
            if (e is SiteDefinitionUpdatedEventArgs updatedArgs)
            {
                var prevSite = updatedArgs.PreviousSite;
                var updatedSite = updatedArgs.Site;
                var settingsRoot = GlobalSettingsRoot;
                if (_contentRepository.GetChildren<IContent>(settingsRoot)
                    .FirstOrDefault(x => x.Name.Equals(prevSite.Name, StringComparison.InvariantCultureIgnoreCase)) is ContentFolder currentSettingsFolder)
                {
                    var cloneFolder = currentSettingsFolder.CreateWritableClone();
                    cloneFolder.Name = updatedSite.Name;
                    _contentRepository.Save(cloneFolder);
                    return;
                }
            }


            CreateSiteFolder(e.Site);
        }

        private void PublishedContent(object sender, ContentEventArgs e)
        {
            if (!(e?.Content is SettingsBase))
            {
                return;
            }

            var parent = _contentRepository.Get<IContent>(e.Content.ParentLink);
            var site = _siteDefinitionRepository.Get(parent.Name);

            var id = site?.Id;
            if (id == null || id == Guid.Empty)
            {
                return;
            }
            UpdateSettings(id.Value, e.Content);
            RaiseEvent(new SettingEventData
            {
                SiteId = id.ToString(),
                ContentId = e.Content.ContentGuid.ToString()
            });
        }

        private Guid ResolveSiteId()
        {
            var request = _httpContext()?.Request;
            if (request == null)
            {
                return Guid.Empty;
            }
            var site = _siteDefinitionResolver.Get(request);
            return site?.Id ?? Guid.Empty;
        }

        private void SettingsEvent_Raised(object sender, EventNotificationEventArgs e)
        {
            // don't process events locally raised
            if (e.RaiserId != _raiserId && e.Param is SettingEventData settingUpdate)
            {
                var content = _contentRepository.Get<IContent>(Guid.Parse(settingUpdate.ContentId));
                if (content != null)
                {
                    UpdateSettings(Guid.Parse(settingUpdate.SiteId), content);
                }
            }
        }

        private void RaiseEvent(SettingEventData message)
        {
            _eventRegistry.Get(EventId).Raise(_raiserId, message);
        }
    }
}

If you have any question let me know. I hope it will help someone and as always keep learning !!!

Written by:

Jorge Cardenas

Developer with several years of experience who is passionate about technology and how to solve problems through it.

View All Posts

Leave a Reply