Language Manager: Replace Content with Unpublished Version
When using Language Manager (LM) to duplicate content from other language. It always get content from Published or Common Draft version. There is a customer they want to get content from Ready To Publish version instead. To change the behavior we can intercept the LanguageBranchManager service. In IConfigurableModule you add following line:
public void ConfigureContainer(ServiceConfigurationContext context)
{
context.Services.Intercept<ILanguageBranchManager>((locator, defaultManager) => new MyLanguageBranchManager(defaultManager));
}
And implment the MyLanguageBranchManager, most of methods we should forward to original service to process. We just modify the [CopyDataFromMasterBranch] method for copying content from Ready To Publish version instead of Published/CommonDraft one. Below is the code for doing this:
public bool CopyDataFromMasterBranch(ContentReference contentReference, string fromLanguageID, string toLanguageID, Func<object, object> transformOnCopyingValue, out ContentReference createdContentLink, bool autoPublish = false)
{
fromLanguageID = fromLanguageID.Trim();
toLanguageID = toLanguageID.Trim();
createdContentLink = null;
// get ReadyToPublish version here to process copying, fallback to Published or CommonDraft version
var masterContent = GetReadyForPublishVersion(contentReference, fromLanguageID) ?? GetPublishedOrCommonDraftVersion(contentReference, fromLanguageID);
var destContent = GetPublishedOrCommonDraftVersion(contentReference, toLanguageID);
if (masterContent == null)
{
throw new ContentNotFoundException(contentReference);
}
if (destContent == null)
{
CreateLanguageBranch(contentReference, toLanguageID, out createdContentLink);
}
var createdDestContent = contentRepository.Service.Get<IContent>(contentReference.ToReferenceWithoutVersion(), new LanguageSelector(toLanguageID));
createdDestContent = (createdDestContent as IReadOnly).CreateWritableClone() as IContent;
#region process for Name, PageURLSegment property of both page and block
if (transformOnCopyingValue == null)
{
createdDestContent.Name = masterContent.Name;
}
else
{
createdDestContent.Name = transformOnCopyingValue(masterContent.Name) as string;
if (masterContent.Property["PageURLSegment"] != null)
{
string url = transformOnCopyingValue(masterContent.Property["PageURLSegment"].ToWebString().Replace('-', ' ')) as string; /* the result from Bing */
url = Regex.Replace(url, @"\s+", " ", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant).Trim(); // convert multiple spaces into one space
url = Regex.Replace(url, @"\s", "-", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); // Replace spaces by dashes
url = Regex.Replace(url, @"[^a-z0-9~_\-\.]", matchEvaluatorRandomReplace, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); // Remove all non valid chars
createdDestContent.Property["PageURLSegment"].Value = url;
}
}
#endregion
#region process all LanguageSpecific, non-meta, editable properties
foreach (PropertyData destProp in createdDestContent.Property.Where(pd =>
(pd.IsLanguageSpecific && !pd.IsMetaData && !pd.IsReadOnly) // find all non-metadata properties, which is editable, language specific
))
{
var nestedContentData = destProp.Value as IContentData; // block
if (null != nestedContentData) // if is block, ...
{
CopyDataForNestedContentRecursive(masterContent.Property[destProp.Name] as IContentData, nestedContentData, transformOnCopyingValue);
}
else
{
// some data type and its property type (e.g. CategoryList & PropertyCategory) implements IModifiedTrackable and then a call to IsModified
// (leading to IReadOnly.ThrowIfReadOnly) from ContentRepository.Save below, so that must use WritableClone value
var srcPropValue = masterContent.Property[destProp.Name].Value;
destProp.Value = srcPropValue is IReadOnly ? ((IReadOnly)srcPropValue).CreateWritableClone() : srcPropValue;
if (transformOnCopyingValue != null && destProp.Value != null)
{
var translatedText = transformOnCopyingValue(destProp);
TryToAssignStringToProperty(translatedText, destProp);
}
}
} // end foreach prop in page level
#endregion
// skip validation, because translate might be failed sometime.
var saveFlag = SaveAction.Save | SaveAction.SkipValidation;
saveFlag = (createdDestContent as IVersionable).Status == VersionStatus.Published ?
saveFlag | SaveAction.ForceNewVersion : saveFlag | SaveAction.ForceCurrentVersion;
if (autoPublish)
{
saveFlag = saveFlag | SaveAction.Publish;
}
createdContentLink = contentRepository.Service.Save(createdDestContent, saveFlag, AccessLevel.NoAccess);
contentVersionRepository.Service.SetCommonDraft(createdContentLink);
return true;
// return _defaultLanguageBranchManager.CopyDataFromMasterBranch(contentReference, fromLanguageID, toLanguageID, transformOnCopyingValue, out createdContentLink, autoPublish);
}
private IContent GetReadyForPublishVersion(ContentReference contentLink, string languageID)
{
var contentVersionRepository = ServiceLocator.Current.GetInstance<IContentVersionRepository>();
var versions = contentVersionRepository.List(contentLink, languageID);
var readyToPublishVersion = versions.FirstOrDefault(v => v.Status == VersionStatus.CheckedIn);
var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
if (readyToPublishVersion != null)
{
return contentRepository.Get<IContent>(readyToPublishVersion.ContentLink);
}
return null;
}
/// <summary>
/// Copies the data for nested content recursively.
/// </summary>
/// <param name="sourceContentData">The source content data.</param>
/// <param name="targetContentData">The target content data.</param>
/// <param name="transformOnCopyingValue">perform a transformation on copying property's value</param>
private void CopyDataForNestedContentRecursive(IContentData sourceContentData, IContentData targetContentData, Func<object, object> transformOnCopyingValue)
{
using (IEnumerator<PropertyData> propertyDataEnumerator = targetContentData.Property
.Where(pd => pd.IsLanguageSpecific && !pd.IsMetaData && !pd.IsReadOnly)
.GetEnumerator())
{
while (propertyDataEnumerator.MoveNext())
{
var targetProp = propertyDataEnumerator.Current;
if (targetProp is IContentData)
{
CopyDataForNestedContentRecursive(sourceContentData.Property[targetProp.Name] as IContentData, targetProp as IContentData, transformOnCopyingValue);
}
else
{
// some data type and its property type (e.g. CategoryList & PropertyCategory) implements IModifiedTrackable and then a call to IsModified
// (leading to IReadOnly.ThrowIfReadOnly) from ContentRepository.Save below, so that must use WritableClone value
var srcPropValue = sourceContentData.Property[targetProp.Name].Value;
targetProp.Value = srcPropValue is IReadOnly ? ((IReadOnly)srcPropValue).CreateWritableClone() : srcPropValue;
if (transformOnCopyingValue != null)
{
var translatedText = transformOnCopyingValue(targetProp);
TryToAssignStringToProperty(translatedText, targetProp);
}
}
}
}
}
/// <summary>
/// assign <paramref name="obj"/> to <paramref name="prop"/> might lead to exception because <paramref name="prop"/> cannot accept string >255.
/// We try to shorten it before assigning again.
/// </summary>
/// <param name="obj"></param>
/// <param name="prop"></param>
/// <returns></returns>
private void TryToAssignStringToProperty(object obj, PropertyData prop)
{
try
{
prop.Value = obj;
}
catch (EPiServerException ex)
{
if (ex.Message.Contains("exceeded")) // exceeded 255 characters
{
prop.Value = ((string)obj).Substring(0, 255);
}
else
{
throw;
}
}
}
If you have similar requirement for your site. I hope this will help you a litle bit.
just have couple of my 2 cents:
- why ServiceLocator?
- why ".GetEnumerator()" & "while(..MoveNext())"
- if method starts with "Try.." I would not expect to receive Exception back, bot bool instead
- also I would try to split code into smaller chunks to organize this quite complex code