James Patterson

programmer, photographer, cinema lover, geek

EPiServer Mirroring and File Properties in Dynamic Content

EPiServer Mirroring 2.0 doesn’t handle resources linked to dynamic content plug-ins as well as it maybe should do. When using a property of type PropertyImageUrl, the Mirroring job fails with an invalid value error for the property using the image URL type. One of the reasons for this is seemed to be (on the live server) that the page is being imported before the file is imported and so the property is being set with a value which is a path to a file that doesn’t exist.

I raised a bug report with EPiServer but was told that it was a feature request, not a bug, and wouldn’t be something they would be doing for version 6 of the CMS. I don’t necessarily share this view but that’s another discussion. Options given to me were either stop using Mirroring or wait until CMS 7 is available and upgrade them, neither was really an option.

The Solution

Like I said above, a part of the problem is that the property on the Dynamic Content is being set to a value which is a path to an image file which doesn’t exist on the live server. I originally thought this would just be a case of stopping the error from interrupting the import by creating a new property class deriving from PropertyImageUrl and overriding the ValidateUrl method. However, even though using the new property class results in the error no longer causing the import to hault and the value is set on the property correctly, the file itself doesn’t get ‘mirrored’ at all. Mirroring isn’t intelligently adding assets referenced by Dynamic Content properties.

We can solve this problem by adding some code to run during the export process. This code will search the properties which are content areas (properties of type PropertyXhtmlString) of the pages being exported for Dynamic Content plug-ins. When we come across an image property (PropertyImageUrl) on any Dynamic Contents found we add the referenced file to the collection of files to be exported and, thus, be imported into the live environment.

The Code

The following code is the custom property class which derives straight from PropertyImageUrl. The reason we have this is only to perform our own logic for ValidateUrl. Instead of throwing an exception, we log it. This type then needs to be used in place of any instance of PropertyImageUrl in your Dynamic Content plug-ins.

public class PropertyDCImageUrl : EPiServer.SpecializedProperties.PropertyImageUrl 
{
private static ILog logger = LogManager.GetLogger(typeof( PropertyDCImageUrl));

protected override void ValidateUri(Uri uri)
{
try
{
base.ValidateUri(uri);
}
catch (FileNotFoundException ex)
{
// Log this error.
logger.Warn(ex.Message, ex);
}
}
}

The code above is pretty straight forward, so I’m not going to explain this further. If you have any queries I’ll be happy to help.

Next we need the code to run on export. To do this we create an initialisation module to attach to the appropriate Mirroring event, in is case we’ll want to run code for the PageExporting event.

[InitializableModule]
[ModuleDependency(typeof(DynamicDataTransferHandler))]
public class MirroringDynamicContentImageUrl : IInitializableModule
{
private static readonly ILog logger = LogManager.GetLogger(typeof(MirroringDynamicContentImageUrl));

public void Initialize(InitializationEngine context)
{
DataExporter.PageExporting += new PageExportingEventHandler(DataExporter_PageExporting);
}

public void Preload(string[] parameters) { }

public void Uninitialize(InitializationEngine context)
{
DataExporter.PageExporting -= new PageExportingEventHandler(DataExporter_PageExporting);
}

protected void DataExporter_PageExporting(DataExporter dataExporting, PageExportingEventArgs e)
{
// Code goes here.
}
}

In the PageExporting event we can analyse the XHTML page properties, their values and discover any dynamic content plug-ins which lie within them. In the DataExporter_PageExporting method we add the following code.

var page = DataFactory.Instance.GetPage(e.PageReference);

// Search each page property for dynamic content instances.
foreach (PropertyData property in page.Property)
{
var xhtmlProperty = property as PropertyXhtmlString;

// We only want to go into properties which are PropertyXhtmlString.
if (xhtmlProperty != null && !xhtmlProperty.IsNull && (xhtmlProperty.Value as string) != null)
{
var body = new HtmlDocument();

body.LoadHtml(xhtmlProperty.Value as string);

// For all dynamic content controls, check all properties for image references.
foreach (IDynamicContent dynamicContent in this.GetDynamicContents(body))
{
foreach (PropertyData dcProperty in dynamicContent.Properties)
{
// If the property is an image property and has a value,
// add the value to the file transfer handler.
if (dcProperty is PropertyImageUrl && !dcProperty.IsNull)
dataExporting.FileTransfer.AddFile(dcProperty.Value as string);
}
}
}
}

Here we are iterating over each of the properties of the page being exported. If the property is one which could possibly contain a Dynamic Content plug-in, an XHTML property, then we process its value to attempt to find any mark-up references to plug-ins. If any dynamic content plug-ins have been found, we iterate through their properties finding any which are of the type PropertyImageUrl and have a value. We then add any value we find to the data exporter file collection.

During this process we are using a couple of helper methods. One of these is GetDynamicContents which takes the HtmlDocument object and returns any instances of IDynamicContent classes it finds. Within this method we find all nodes within the object which has an attribute ‘class’ with a value of ‘epi_dc’. We then create an instance of a dynamic content object from the nodes found, returning them in a list.

private IList GetDynamicContents(HtmlDocument htmlDoc)
{
IList dynamicContentCollection = new List();

// Get all html nodes which have a class of epi_dc.
IEnumerable dcNodes = htmlDoc.DocumentNode.Descendants()
.Where(n => n.Attributes["class"] != null && n.Attributes["class"].Value == "epi_dc");

foreach (HtmlNode item in dcNodes)
{
// Create an instance of the dynamic content and add to the collection for returning.
IDynamicContent dynamicContent = this.CreateDynamicContentFromNode(item);

if (dynamicContent != null) dynamicContentCollection.Add(dynamicContent);
}

return dynamicContentCollection;
}

If there is an 'EPiServer way' of getting all Dynamic Content instances from a value of a property then please let me know. I had a scout around in the API and a bit of time reflecting the EPiServer assemblies but couldn't find anything.

The other helper method is used within the method above and is called CreateDynamicContentFromNode. This method simply attempts to get the values of three attributes from the HtmlNode object which are required to create an instance of a Dynamic Content. 

private IDynamicContent CreateDynamicContentFromNode(HtmlNode htmlNode)
{
// For some reason the state stored within the HTML for DCs created using the
// UserControl method doesn't work when passed into the CreateDynamicContent method.
// DCs created in this fashion, with Image URL properties, will not be read.
try
{
return DynamicContentFactory.Instance.CreateDynamicContent(
htmlNode.Attributes["data-dynamicclass"].Value,
htmlNode.Attributes["data-state"].Value,
htmlNode.Attributes["data-hash"].Value,
true);
}
catch (FormatException ex)
{
logger.Warn(ex.Message, ex);

return null;
}
}

As mentioned within the comments of the code above, this doesn’t seem to work for all Dynamic Contents within XHTML properties. For some reason the state causes and exception if the Dynamic Content has been implemented in your project in the ‘UserControl’ way, the Dynamic Content must be, or be a deriving type of, a class which inherits from IDynamicContent. This might be down to the fact I'm not going about it the right way (almost certain of this) - I don't know but if you do then please feel free to share!

As you may have guessed already, to do all this work with the mark-up I’m using Html Agility Pack. So on top of the EPiServer references, you’ll need to reference this or simply add it via NuGet. The agility pack just makes it easier to iterate through the HTML tags to find the tags representing the dynamic content data we need. The agility pack HtmlDocument object is also more forgiving with non-perfect XHTML than using XmlDocument.

To get hold of the full code for both the custom property class and the initialisation module I've made them available as gists.

© 2017 James Patterson