Automatically Populating Page or Block Type Properties for a Language Branch in EPiServer 7

March 2022 Update: This post was originally written for EPiServer version 7. In 2021, Episerver (now known as Optimizely) released version 12. While some parts of this post are outdated, other parts are still relevant and may be helpful. No content in this post has been changed for the current version.

The creation and editing of content for multiple language sites in EPiServer 7 has recently become a topic of conversation in some of our projects. We occasionally receive feedback from our clients who are looking for ways to reduce the amount of time it takes to enter content, while also being consistent with the page and block layout.

One big change with the new Edit Mode in EPiServer 7, which was available in EPiServer 6 (and technically is still available in the old Edit Mode), is that feature to compare the language branches for a page side-by-side has been removed. This was useful for editors who need to keep the content for pages consistent between each language branch, as it allowed them to easily compare the different language branches.

So to help this, we created a property attribute called [AutoPopulateLanguageBranch]. Whenever a editor translates a page or block, this copies the values from the master language branch to the new language branch for the decorated page or block type properties. This keeps the content consistent between the language branches, which, at the same time, helps speed up the process of entering content.

The Code

First of all, the ability to translate blocks for different language branches was added with the release of EPiServer 7.1. If you are not running EPiServer 7.1, I recommend upgrading your project before you start using this attribute.

We just need three things: the [AutoPopulateLanguageBranch] attribute to decorate our properties, an initialization module to monitor when we should copy our property values, and an IContent extension method to actually copy the decorated property values.

AutoPopulateLanguageBranchAttribute.cs

We start with a simple attribute to decorate our page type properties and block type properties.

[AttributeUsage(AttributeTargets.Property)]
public class AutoPopulateLanguageBranchAttribute : Attribute
{
}

AutoPopulateLanguageBranchInitializationModule.cs

The first main piece of this is the initialization module. This attaches (and detaches) the InitializeContentLanguageBranch method to the LoadedDefaultContent event of the DataFactory, which "occurs when a new content item has been created and initialized". This method is essentially just some preventative checks and content data setup. We need to make sure we have legitimate content, and that the content we are working with is not on the master language branch. Then, we grab the content data from the master language branch, so we have something to copy over to the new language branch. Once we have everything, we send it over to ContentExtensions for the copying of property values.

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class AutoPopulateLanguageBranchInitializationModule : IInitializableModule
{
    public void Initialize(EPiServer.Framework.Initialization.InitializationEngine context)
    {
        DataFactory.Instance.LoadedDefaultContent += InitializeContentLanguageBranch;
    }

    public void Preload(string[] parameters)
    {
    }

    public void Uninitialize(EPiServer.Framework.Initialization.InitializationEngine context)
    {
        DataFactory.Instance.LoadedDefaultContent -= InitializeContentLanguageBranch;
    }

    private void InitializeContentLanguageBranch(object sender, ContentEventArgs e)
    {
        if (e.Content != null && e.Content.ContentGuid != Guid.Empty)
        {
            var contentLanguage = new ContentLanguageEventArgs(e.Content);
            if (contentLanguage != null && !contentLanguage.IsMasterLanguageBranch)
            {
                var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
                var masterLanguageContent = contentLoader.Get<IContent>(contentLanguage.ContentLink, new LanguageSelector(contentLanguage.MasterLanguageBranch));
                if (masterLanguageContent != null)
                {
                    e.Content.InitializeDefaultPropertyData(masterLanguageContent);
                }
            }
        }
    }
}

ContentExtensions.cs

The second main piece is the extension method InitializeDefaultPropertyData for the IContent interface. Once again, we do some preventative checks, then get the page/block Type, so we can look through the properties and see which ones are decorated with the [AutoPopulateLanguageBranch] attribute. After we have on all the properties, we simply copy the Value over from the sourceContent to the targetContent. Finally, we return the targetContent back to the module.

public static class ContentExtensions
{
    public static IContent InitializeDefaultPropertyData(this IContent targetContent, IContent sourceContent)
    {
        if (sourceContent == null ||
            sourceContent.GetType() != targetContent.GetType() ||
            sourceContent.GetType().BaseType == null)
        {
            return targetContent;
        }

        var type = sourceContent.GetType().BaseType;

        foreach (var property in type.GetProperties().Where(p => p.GetCustomAttributes(typeof(AutoPopulateLanguageBranchAttribute), true).Length > 0))
        {
            if (property != null &&
                targetContent.Property[property.Name] != null &&
                targetContent.Property[property.Name].IsLanguageSpecific)
            {
                targetContent.Property[property.Name].Value = sourceContent.Property[property.Name].Value;
            }
        }

        return targetContent;
    }
}

The Usage

Using the [AutoPopulateLanguageBranch] attribute is really straightforward... Just decorate the properties that you want to copy over in your page types or block types.

So for a page type, it could look something like this:

[ContentType(GUID = "00000000-0000-0000-0000-000000000000")]
public class StandardPage : PageData
{
    [CultureSpecific]
    [AutoPopulateLanguageBranch]
    public virtual XhtmlString MainBody { get; set; }

    [CultureSpecific]
    [AutoPopulateLanguageBranch]
    public virtual ContentArea MainContentArea { get; set; }
}

Or for a block type:

[ContentType(GUID = "00000000-0000-0000-0000-000000000000")]
public class ImageBlock : BlockData
{
    [CultureSpecific]
    [AutoPopulateLanguageBranch]
    [UIHint(UIHint.Image)]
    public virtual Url Image { get; set; }
}

It's pretty obvious that the [AutoPopulateLanguageBranch] attribute is commonly seen next to the [CultureSpecific] attribute. If the property is not decorated with [CultureSpecific], then it will be skipped during the copy process.

A nice part about this attribute is that it can populate a page's ContentArea property with the blocks that are placed in it, and if the block has not yet been translated to the new language branch, it will fallback and show the content data for the block's master language branch. This is what the block will look like on a page in Edit Mode:

Untranslated block

That gives the editor a really good visual cue that the block needs to be translated to the new language branch.