Use culture specific MediaData properties

On courtesy of Arlanet

In the latest version of EPiServer it’s not (yet) possible to use culture specific properties for MediaData types.

The answer of Linus Ekstrom on my forum post: ‘The answer is no, currently all media data are non-localizable. There was several reasons for this but the main one was that we did not have time to manage all bugs/quirks that appear when you need to deal with localizable media. Therefore, the decition was to handle media as non-localizable content (just as the old VPP based file system), as least for the EPiServer 7.5 release.

When I was encountering this problem I was looking for an alternative. I used the following solution:

The first step is to handling EPiServer events for loading and saving MediaData objects. For this I created a initialization module:

/// <summary>
/// Media initialization
/// </summary>
[InitializableModule]
public class MediaInitialization : IInitializableModule
{
    private IMediaTranslationService mediaTranslationService;
 
    /// <summary>
    /// Initialize
    /// </summary>
    /// <param name="context"></param>
    public void Initialize(EPiServer.Framework.Initialization.InitializationEngine context)
    {
        this.mediaTranslationService = ServiceLocator.Current.GetInstance<IMediaTranslationService>();
 
        DataFactory.Instance.LoadedContent += Instance_LoadedContent;
        DataFactory.Instance.LoadedChildren += Instance_LoadedChildren;
        DataFactory.Instance.SavingContent += Instance_SavingContent;
    }

The events LoadedContent, LoadedChildren and SavingContent are fired each time a IContent object is loaded or saved.

#region Event Handlers
 
/// <summary>
/// Children loaded event handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Instance_LoadedChildren(object sender, ChildrenEventArgs e)
{
    if (e.ChildrenItems != null)
    {
        for (var i = 0; i < e.ChildrenItems.Count; i++)
        {
            var item = e.ChildrenItems[i];
            if (item != null && item is IMediaTranslation) // Check if IContent item is of type IMediaTranslation
            {
                e.ChildrenItems[i] = LoadMediaTranslation(item); // Set translated object
            }
        }
    }
}
 
/// <summary>
/// Loaded content event handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Instance_LoadedContent(object sender, ContentEventArgs e)
{
    if (e.Content != null && e.Content is IMediaTranslation)
    {
        e.Content = LoadMediaTranslation(e.Content); // Set translated object
    }
}
 
 
/// <summary>
/// Saving content event handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Instance_SavingContent(object sender, ContentEventArgs e)
{
    if (e.Content != null && e.Content is IMediaTranslation)
    {
        var mediaFile = (IMediaTranslation)((MediaData)e.Content).CreateWritableClone();
 
        this.mediaTranslationService.SaveTranslation(mediaFile, ContentLanguage.PreferredCulture.Name); // Save translated properties
    }
}
 
#endregion
 
#region Private Methods
 
/// <summary>
/// Load media translation for a media data object
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
private IMediaTranslation LoadMediaTranslation(IContent content)
{
    var imageFile = (IMediaTranslation)((MediaData)content).CreateWritableClone();
 
    return this.mediaTranslationService.Translate(imageFile, ContentLanguage.PreferredCulture.Name);
}
 
#endregion

In the event handlers the Content property is checked if the object is of type IMediaTranslation. This interface indicates that the IContent contains multi language properties. The event handlers communicates with a service layer, MediaTranslationService, at this moment the service doesn’t have functionality. The service only calls the associated methods of the MediaTranslationRepository.

The IMediaTranslation interface contain methods for the translation and mapping the properties of the IContent to the PropertyBag object. The PropertyBag is of course saved in Dynamic Data Store.

For this example I’ve an implementation of the IMediaTranslation for an ImageData type:

/// <summary>
/// IMedia translation interface
/// </summary>
public interface IMediaTranslation : IContent
{
    /// <summary>
    /// Translate method
    /// </summary>
    /// <param name="pb"></param>
    void Translate(PropertyBag pb);
 
    /// <summary>
    /// Get property bag
    /// </summary>
    /// <param name="propertyBag"></param>
    /// <returns></returns>
    void GetPropertyBag(ref PropertyBag propertyBag);
 
    /// <summary>
    /// Get store mappings
    /// </summary>
    /// <returns></returns>
    Dictionary<string, Type> GetStoreMappings();
}

The ImageFile class contains the Title property that should be a multi-language property. The Translate method takes a PropertyBag as parameter and maps it to the associated property. The Translate method is used when an IContent (MediaData) is loaded. The GetPropertyBag method sets the PropertyBag object with current language property values, this is used for saving an IContent. The GetStoreMappings method returns a key value dictionary, this is used by the Dynamic Data Store to insert values.

For the actual loading and saving of the property values in DynamicDataStore I created the MediaTranslationRepository:

/// <summary>
/// Media translation repository
/// </summary>
public class MediaTranslationRepository : IMediaTranslationRepository
{
    private const string storePrefix = "Translation_{0}";
  
    /// <summary>
    /// Get media translation by media and language code
    /// </summary>
    /// <param name="media"></param>
    /// <param name="language"></param>
    /// <returns></returns>
    public IMediaTranslation Get(IMediaTranslation media, string language)
    {
        PropertyBag propertyBag = null;
  
        string cacheKey = GetMediaTranslationCacheKey(media, language);
        if (CacheManager.Get(cacheKey) != null) // Check if an item is present in the cache
            propertyBag = (PropertyBag)CacheManager.Get(cacheKey);
  
        if (propertyBag == null)
        {
            var mediaStoreMappings = media.GetStoreMappings();
            if (mediaStoreMappings.Count == 0)
                return media;
  
            var store = DynamicDataStoreFactory.Instance.CreateStore(string.Format(storePrefix, media.GetType().Name),
                GetBaseStoreMapping(mediaStoreMappings));
  
            propertyBag = Find(store, media.ContentGuid, language);
            if (propertyBag == null)
            {
                media.Translate(new PropertyBag());
                return media;
            }
  
            CacheManager.Insert(cacheKey, propertyBag); // Insert in cache
        }
        media.Translate(propertyBag);
  
        return media;
    }
  
    /// <summary>
    /// Save media translation file if not exists in database then new record is created
    /// </summary>
    /// <param name="media"></param>
    /// <param name="language"></param>
    public void Save(IMediaTranslation media, string language)
    {
        string cacheKey = GetMediaTranslationCacheKey(media, language);
  
        var mediaStoreMappings = media.GetStoreMappings();
        if (mediaStoreMappings.Count == 0)
            return;
  
        var store = DynamicDataStoreFactory.Instance.CreateStore(string.Format(storePrefix, media.GetType().Name), GetBaseStoreMapping(mediaStoreMappings));
        var propertyBag = Find(store, media.ContentGuid, language);
  
        if (propertyBag == null)
        {
            propertyBag = new PropertyBag();
            propertyBag.Add("ContentGuid", media.ContentGuid);
            propertyBag.Add("ContentLanguage", language);
        }
  
        media.GetPropertyBag(ref propertyBag);
  
        store.Save(propertyBag); // Save PropertyBag in dynamic data store
  
        if (CacheManager.Get(cacheKey) != null)
            CacheManager.Remove(cacheKey); // Remove from cache
  
        CacheManager.Insert(cacheKey, propertyBag); // Insert in cache
    }
  
    /// <summary>
    /// Get base store mapping
    /// </summary>
    /// <param name="storeMapping"></param>
    /// <returns></returns>
    private IDictionary<string, Type> GetBaseStoreMapping(IDictionary<string, Type> storeMapping)
    {
        storeMapping.Add("ContentGuid", typeof(Guid));
        storeMapping.Add("ContentLanguage", typeof(string));
  
        return storeMapping;
    }
  
    /// <summary>
    /// Get media translation cache key
    /// </summary>
    /// <param name="media"></param>
    /// <param name="language"></param>
    /// <returns></returns>
    private string GetMediaTranslationCacheKey(IMediaTranslation media, string language)
    {
        return string.Format("media_translation_{0}_{1}", media.ContentGuid, language);
    }
  
    /// <summary>
    /// Get PropertyBag object from DynamicDataStore
    /// </summary>
    /// <param name="store"></param>
    /// <param name="contentGuid"></param>
    /// <param name="language"></param>
    /// <returns></returns>
    private PropertyBag Find(DynamicDataStore store, Guid contentGuid, string language)
    {
        var parameters = new Dictionary<string, object>();
        parameters.Add("ContentGuid", contentGuid);
        parameters.Add("ContentLanguage", language);
  
        var propertyBag = new PropertyBag();
  
        var items = store.FindAsPropertyBag(parameters);
        var list = items as IList<PropertyBag> ?? items.ToList();
  
        return list.FirstOrDefault();
    }
}

The Get method accepts a IMediaTranslation (IContent) object and a language. First the cache is being called. A DynamicDataStore object is returned by the CreateStore method, this method accepts a name. The implementation type of the media (IMediaTranslation type) is being used for the name of the store. This way the repository can be reused for multiple MediaData implementations that’s implementing the IMediaTranslation interface.

For loading and saving a PropertyBag an unique identifier is being used. This unique identifier contains two values the ContentGuid and the ContentLanguage. After the PropertyBag is returned by the DynamicDataStore, the Translate method is called for doing the actually mapping.

The Save method is responsible for saving the properties in DynamicDataStore.

To finialize this example I created a HomeController. The HomePage contains a ContentReference property with the UIHint attribute set to Image. In my HomeController I get an implementation of the ImageFile class by calling the Get method of the IContentLoader. This method call will trigger the LoadedContent event and as I explained earlier this event will be handled by the initialization module. The repository then will load the associated property values according the current language. The ImageFile will handle the mapping of the PropertyBag to the actual properties of the ImageFile class.

/// <summary>
/// Home page controller
/// </summary>
public class HomePageController : PageController<HomePage>
{
    private IContentLoader contentLoader;
 
    /// <summary>
    /// Public constructor
    /// </summary>
    /// <param name="contentLoader"></param>
    public HomePageController(IContentLoader contentLoader)
    {
        this.contentLoader = contentLoader;
    }
 
    /// <summary>
    /// Index action
    /// </summary>
    /// <param name="currentPage"></param>
    /// <returns></returns>
    public ActionResult Index(HomePage currentPage)
    {
        var model = new HomeViewModel(currentPage);
        var image = contentLoader.Get<ImageFile>(currentPage.Image1);
        model.ImageTitle = image.Title;
        model.CurrentLanguage = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
 
        return View(model);
    }
}

Below the home page view for displaying the Title property of the ImageFile class and a navigation to switch language.

@model HomeViewModel
@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
<h1>Home</h1>
<strong>@Model.ImageTitle</strong><br />
<img src="@Url.ContentUrl(Model.Page.Image1)" />
<div>
    <strong>Language</strong>
    <ul>
        <li @(Model.CurrentLanguage.Equals("nl", StringComparison.InvariantCultureIgnoreCase) ? "class=active" : string.Empty)><a href="/nl">Dutch</a></li>
        <li @(Model.CurrentLanguage.Equals("en", StringComparison.InvariantCultureIgnoreCase) ? "class=active" : string.Empty)><a href="/">English</a></li>
    </ul>
</div>

 

The actually translation can be done in the CMS. Just choose a language and browse to the image file. Open the properties panel and edit the property values. If you switch to another language and open the properties panel again you will see that the multi language properties are empty.

Dutch translation:

Switch language:

English translation:

Please be free to ask questions or give feedback!

Share this article
Comments
15 December 2015 Vladimir

Nice but slow. Take a look at https://gregwiechec.com/2015/07/localizable-media-assets/