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

Here is how my config patch and processor were like when I finished the initial approach.

The config patch

<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>

The processor

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


why meme

But why does this happen?


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! 




Comments

Post a Comment

Thanks for commenting!

Popular posts from this blog

How to use SXA Content Tokens in a JSS solution