RenderField pipeline - GetImageFieldValue processor returns empty when image comes from Content Hub
Intro
If you are trying to customize the way how Sitecore renders an image field globally in your solution, the most common approach to do that is by creating a pipeline processor for Sitecore.Pipelines.RenderField pipeline and configure it to be invoked right after or instead of GetImageFieldValue processor.
A really good example for that is when you need to add a loading=”lazy” attribute to all of your images, because doing that in all Field helper call, like shown below, can be quite challenging if you have many places to update, and you probably have.
@Html.Sitecore().Field("Thumbnail Image", Model.Item, new { loading = "lazy" })
P.S My intention here is not teaching you how to implement a lazy loading mechanism, but I’ll use this as an example since it’s a very common use case. If you need some idea of how to do it just read this article
That said, the best option we have is implementing a processor for FieldRender pipeline, and I did it!
However, the way I choosed to do that lead me to an issue that I’ve never seen before.
The method GetInnerImageItem from Sitecore.Kernel dll returns null when the value is an image from ContentHub.
The initial approach
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/"> <sitecore> <pipelines> <renderField> <processor patch:instead="processor[@type='Sitecore.Pipelines.RenderField.GetImageFieldValue, Sitecore.Kernel']" type="Foundation.Pipelines.ImageProcessing, Foundation.Pipelines"/> </renderField> </pipelines> </sitecore> </configuration>
public class ImageProcessing : GetImageFieldValue { public override void Process(RenderFieldArgs args) { if (this.ShouldExecute(args)) { base.Process(args); args.Result.FirstPart = this.AddLoadingAttribute(args.Result.FirstPart); } } public bool ShouldExecute(RenderFieldArgs args) { if (!base.IsImage(arg)) return false; if (!Sitecore.Context.PageMode.IsNormal) return false; if (Sitecore.Context.Site == null) return false; if (Sitecore.Context.Item == null) return false; if (args.Result != null && string.IsNullOrEmpty(args.Result.FirstPart)) return false; return true; } private string AddLoadingAttribute(string tag) { if (tag.Contains("loading")) { var pattern = "loading\\s*=\\s*['|\"].+?['\"]"; var replacement = " loading='lazy'"; tag = Regex.Replace(tag, pattern, replacement); } else { var pattern = "\\s*/*\\s*>"; var replacement = " loading='lazy'/>"; tag = Regex.Replace(tag, pattern, replacement); } return tag; } }
Pay attention I’ve configured to run ImageProcessing as
patch:instead of GetImageFieldValue, due to inheritance I can call Process method from base class to make sure nothing was left behind.
The problem
The above approach works fine if you don't have a ContentHub instance integrated to your solution, however, that's not my case.
When base.Process(args) is invoked the args.Result.FirstPart property is set as empty string
Well, to answer that let’s take a look into Sitecore.Kernel.dll file using
a decompiling tool like JetBrains dotPeek.
The Process method
public virtual void Process(RenderFieldArgs args) { Assert.ArgumentNotNull((object) args, nameof (args)); if (!this.IsImage(args)) return; ImageRenderer renderer = this.CreateRenderer(); this.ConfigureRenderer(args, renderer); this.SetRenderFieldResult(renderer.Render(), args); }
The ConfigureRenderer method
protected virtual void ConfigureRenderer(RenderFieldArgs args, ImageRenderer imageRenderer) { Item itemToRender = args.Item; imageRenderer.Item = itemToRender; imageRenderer.FieldName = args.FieldName; imageRenderer.FieldValue = args.FieldValue; imageRenderer.Parameters = args.Parameters; if (itemToRender == null) return; imageRenderer.Parameters.Add("la", itemToRender.Language.Name); this.EnsureMediaItemTitle(args, itemToRender, imageRenderer); }
The EnsureMediaItemTitle and GetInnerImageItem methods
protected virtual void EnsureMediaItemTitle( RenderFieldArgs args, Item itemToRender, ImageRenderer imageRenderer) { if (!string.IsNullOrEmpty(args.Parameters[this.TitleFieldName])) return; Item innerImageItem = this.GetInnerImageItem(args, itemToRender); if (innerImageItem == null) return; Field field = innerImageItem.Fields[this.TitleFieldName]; if (field == null) return; string str = field.Value; if (string.IsNullOrEmpty(str) || imageRenderer.Parameters == null) return; imageRenderer.Parameters.Add(this.TitleFieldName, str); } protected virtual Item GetInnerImageItem(RenderFieldArgs args, Item itemToRender) { Field field = itemToRender.Fields[args.FieldName]; return field == null ? (Item) null : new ImageField(field, args.FieldValue).MediaItem; }
And finally the SetRenderFieldResult method
protected virtual void SetRenderFieldResult(RenderFieldResult result, RenderFieldArgs args) { args.Result.FirstPart = result.FirstPart; args.Result.LastPart = result.LastPart; args.WebEditParameters.AddRange((SafeDictionary<string, string>) args.Parameters); args.DisableWebEditContentEditing = true; args.DisableWebEditFieldWrapping = true; args.WebEditClick = "return Sitecore.WebEdit.editControl($JavascriptParameters, 'webedit:chooseimage')"; }
Let’s debug it (you can copy the methods you want to debug into your class and call them instead of calling from the base class using inheritance)
So, starting from Process method the following call stack will be created
GetInnerImageItem
EnsureMediaItemTitle
ConfigureRenderer
Process
There is a ternary if in the GetInnerImageItem method that is supposed to return the media item attached to the field, but as I told you, we are also using ContentHub to hold some images and in that case, when the image is coming from ContentHub there is no media item attached to the field and this method will return always null, so moving on, in the SetRenderFieldResult method our args.Result.FirstPart and args.Result.LastPart properties will be empty.
Now here is an interesting fact, if you invoke this processor as the last one of your pipeline, no html will be printed and if you run it before the ContentHub's processor your changes will be replaced.
When you are getting image from ContentHub, there is a processor called GetMImageRenderField that adds a few attributes into the result img tag like thumbnailsrc, so that, we can use them from other processors.
The solution
What if I run this ImageProcessing right after the GetMImageRenderField processor?
Precisely!
When working with pipelines the invoking order of your processor's is a quite important point. The processor receives a RenderFieldArgs parameter, do what it has to do and pass the args parameter to the next processor in the pipeline.
So to achieve that, I had to do a quick update on my initial solution just removing the base.Process call and changing the patch config to invoke my processor right after GetMImageRenderField.
Here is how it looked like in the end.
The config patch
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/"> <sitecore> <pipelines> <renderField> <processor patch:after="processor[@type='Sitecore.Connector.ContentHub.DAM.Pipelines.RenderField.GetMImageRenderField, Sitecore.Connector.ContentHub.DAM']" type="Foundation.Pipelines.ImageProcessing, Foundation.Pipelines"/> </renderField> </pipelines> </sitecore> </configuration>
The processor
public class ImageProcessing : GetImageFieldValue { public override void Process(RenderFieldArgs args) { if (this.ShouldExecute(args)) { args.Result.FirstPart = this.AddLoadingAttribute(args.Result.FirstPart); } } public bool ShouldExecute(RenderFieldArgs args) { if (!base.IsImage(arg)) return false; if (!Sitecore.Context.PageMode.IsNormal) return false; if (Sitecore.Context.Site == null) return false; if (Sitecore.Context.Item == null) return false; if (args.Result != null && string.IsNullOrEmpty(args.Result.FirstPart)) return false; return true; } private string AddLoadingAttribute(string tag) { if (tag.Contains("loading")) { var pattern = "loading\\s*=\\s*['|\"].+?['\"]"; var replacement = " loading='lazy'"; tag = Regex.Replace(tag, pattern, replacement); } else { var pattern = "\\s*/*\\s*>"; var replacement = " loading='lazy'/>"; tag = Regex.Replace(tag, pattern, replacement); } return tag; } }
Conclusion
Pay attention on the invoking order of your processors,
they might be replacing or undoing things that you’ve done from a previous processor,
when working with them, I strongly recommend you to disable all the other
processors, leave only the ones that makes sense to your current ongoing task
and once you finish it, enable them again one by one, doing this you’ll find quickly which one
of your existing processors is causing issues.
That’s all guys, I hope this use case make your path to find a solution to your case a little bit short.
See you ahead!
Top!!!
ReplyDelete