Digital Asset Management

Implementing a Custom DAM Connector in Sitecore (part 1)

By Nick Amis

09/12/2023

LinkedIn

DAMs can be powerful tools for maintaining all content in a single repository.  However, out of the box, Sitecore does not support any kind of external image linking within its image field type.  Our goal in this series of posts is to create a framework for implementing a custom connector for external images that can be extended for usage with any DAM. Within the series: 

  • Extending the Image Field Type 
  • Implementing a Custom Dialog for a DAM
  • (Bonus) Creating a Media Handler for Optimizing External Assets using Dianoga 

Extending the Image Field Type 

Our first step in the process to creating a custom DAM connector is to tell Sitecore how to interpret external images.  

Creating a Field Type 

The below is all you will need to create a new field that just contains external URL. This won't tell Sitecore how to interpret the value yet, but it will allow you to read/modify the value of the URL as an attribute on the Image Field.  

For our custom connector, we are only setting an external URL that Sitecore will serve, but you can also do things like store an ID to an asset (and use that at render time with your DAM's API). 

using Sitecore.Data.Fields;
namespace MySite.Fields
{
    public class ExternalUrlImageField: ImageField
    {
        public ExternalUrlImageField(Field innerField): base(innerField)
        {}
        public ExternalUrlImageField(Field innerField, string runtimeValue): base(innerField, runtimeValue)
        {}
        public string ExternalUrl
        {
            get
            {
                return GetAttribute(Constants.Fields.ExternalUrl) ?? string.Empty;
            }
            set
            {
                SetAttribute(Constants.Fields.ExternalUrl, value ?? string.Empty);
            }
        }
        public static implicit operator ExternalUrlImageField(Field field)
        {
            return field == null ? null : new ExternalUrlImageField(field);
        }
    }
}

You can register this type in a patch config: 

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/"> 
<sitecore> 
<fieldTypes> 
<fieldType name="Image" set:type="MySite.Fields.ExternalUrlImageField, MySite" /> 
</fieldTypes> 

Rendering the field 

In order to get Sitecore to start serving external images, we need to substitute a new pipeline for Sitecore's GetImageValue pipeline. This lets us control how Sitecore handles our new External URL attribute. 

First, create a new Image Renderer, which extends from Sitecore's ImageRenderer: 

Unfortunately, you will have to reimplement the majority of the ImageRenderer because Sitecore uses internal values that we can't set ourselves without introducing new variables.

namespace MySite.Pipelines.RenderField
{
    public class ExternalImageRenderer: ImageRenderer
    {
        private string alt;
        private bool allowStretch;
        private bool asSet;
        private string backgroundColor;
        private string border;
        private string className;
        private string database;
        private bool disableMediaCache;
        private bool disableMediaCacheSet;
        private string fieldName;
        private string fieldValue;
        private int height;
        private bool heightSet;
        private string hspace;
        private bool ignoreAspectRatio;
        private ImageField imageField;
        private Item item;
        private string language;
        private int maxHeight;
        private bool maxHeightSet;
        private int maxWidth;
        private bool maxWidthSet;
        private SafeDictionary < string > parameters;
        private float scale;
        private bool scaleSet;
        private string source;
        private bool thumbnail;
        private bool thumbnailSet;
        private string version;
        private string vspace;
        private int width;
        private bool widthSet;
        private bool xhtml;
        public new string FieldName
        {
            get => this.fieldName;
            set
            {
                Assert.ArgumentNotNull((object) value, nameof(value));
                this.fieldName = value;
            }
        }
        public new string FieldValue
        {
            get => this.fieldValue;
            set
            {
                Assert.ArgumentNotNull((object) value, nameof(value));
                this.fieldValue = value;
            }
        }
        public new Item Item
        {
            get => this.item;
            set
            {
                Assert.ArgumentNotNull((object) value, nameof(value));
                this.item = value;
            }
        }
        public new SafeDictionary < string > Parameters
        {
            get => this.parameters;
            set
            {
                Assert.ArgumentNotNull((object) value, nameof(value));
                this.parameters = value;
            }
        }
        public string Source
        {
            get
            {
                return source;
            }
            set
            {
                source = value;
            }
        }
        public override RenderFieldResult Render()
        {
            Item obj = this.Item;
            if (obj == null) return RenderFieldResult.Empty;
            SafeDictionary < string > parameters = this.Parameters;
            if (parameters == null) return RenderFieldResult.Empty;
            this.ParseNode(parameters);
            Field field = obj.Fields[this.FieldName];
            if (field != null)
            {
                this.imageField = new ImageField(field, this.FieldValue);
                this.ParseField(this.imageField);
                this.AdjustImageSize(this.imageField, this.scale, this.maxWidth, this.maxHeight, ref this.width, ref this.height);
            }
            SiteContext site = Context.Site;
            if (string.IsNullOrEmpty(this.Source) && (string.IsNullOrEmpty(this.source) || this.IsBroken(this.imageField)) && site != null && site.DisplayMode == DisplayMode.Edit)
            {
                this.source = this.GetDefaultImage();
                this.className += " scEmptyImage";
                this.className = this.className.TrimStart(' ');
            }
            if (string.IsNullOrEmpty(this.source)) return RenderFieldResult.Empty;
            string source = this.GetSource();
            StringBuilder stringBuilder = new StringBuilder("<img");
            FieldRendererBase.AddAttribute(stringBuilder, "src", source);
            FieldRendererBase.AddAttribute(stringBuilder, "border", this.border);
            FieldRendererBase.AddAttribute(stringBuilder, "hspace", this.hspace);
            FieldRendererBase.AddAttribute(stringBuilder, "vspace", this.vspace);
            FieldRendererBase.AddAttribute(stringBuilder, "class", this.className);
            FieldRendererBase.AddAttribute(stringBuilder, "alt", HttpUtility.HtmlAttributeEncode(this.alt), this.xhtml);
            if (this.width > 0) FieldRendererBase.AddAttribute(stringBuilder, "width", this.width.ToString());
            if (this.height > 0) FieldRendererBase.AddAttribute(stringBuilder, "height", this.height.ToString());
            this.CopyAttributes(stringBuilder, parameters);
            stringBuilder.Append(" />");
            return new RenderFieldResult(stringBuilder.ToString());
        }
        protected override void AdjustImageSize(ImageField imageField, float imageScale, int imageMaxWidth, int imageMaxHeight, ref int w, ref int h)
        {
            Assert.ArgumentNotNull((object) imageField, nameof(imageField));
            int width = MainUtil.GetInt(imageField.Width, 0);
            int height = MainUtil.GetInt(imageField.Height, 0);
            if (width == 0 || height == 0) return;
            Size size = new Size(w, h);
            Size imageSize = new Size(width, height);
            Size maxSize = new Size(imageMaxWidth, imageMaxHeight);
            Size finalImageSize = this.GetFinalImageSize(this.GetInitialImageSize(imageSize, imageScale, size), size, maxSize);
            w = finalImageSize.Width;
            h = finalImageSize.Height;
        }
        protected override void CopyAttributes(StringBuilder result, SafeDictionary < string > attributes)
        {
            Assert.ArgumentNotNull((object) result, nameof(result));
            Assert.ArgumentNotNull((object) attributes, nameof(attributes));
            foreach(KeyValuePair < string, string > attribute in (SafeDictionary < string, string > ) attributes)
            {
                if (attribute.Key != "field" && attribute.Key != "select" && attribute.Key != "outputMethod") FieldRendererBase.AddAttribute(result, attribute.Key, WebUtil.SafeEncode(attribute.Value));
            }
        }
        protected override string Extract(SafeDictionary < string > values, params string[] keys)
        {
            Assert.ArgumentNotNull((object) values, nameof(values));
            Assert.ArgumentNotNull((object) keys, nameof(keys));
            foreach(string key in keys)
            {
                string str = values[key];
                if (str != null)
                {
                    values.Remove(key);
                    return str;
                }
            }
            return (string) null;
        }
        protected override string GetDefaultImage() => GetSource();
        protected override Size GetFinalImageSize(Size imageSize, Size size, Size maxSize)
        {
            if (maxSize.IsEmpty) return imageSize;
            if (maxSize.Width > 0 && imageSize.Width > maxSize.Width)
            {
                if (size.Height == 0) imageSize.Height = (int) Math.Round((double) maxSize.Width / (double) imageSize.Width * (double) imageSize.Height);
                imageSize.Width = maxSize.Width;
            }
            if (maxSize.Height > 0 && imageSize.Height > maxSize.Height)
            {
                if (size.Width == 0) imageSize.Width = (int) Math.Round((double) maxSize.Height / (double) imageSize.Height * (double) imageSize.Width);
                imageSize.Height = maxSize.Height;
            }
            return imageSize;
        }
        protected override Size GetInitialImageSize(Size imageSize, float imageScale, Size size)
        {
            if ((double) imageScale > 0.0) return new Size(this.Scale(imageSize.Width, imageScale), this.Scale(imageSize.Height, imageScale));
            if (size.IsEmpty || size == imageSize) return imageSize;
            if (size.Width == 0)
            {
                float scaleNumber = (float) size.Height / (float) imageSize.Height;
                return new Size(this.Scale(imageSize.Width, scaleNumber), size.Height);
            }
            if (size.Height != 0) return new Size(size.Width, size.Height);
            float scaleNumber1 = (float) size.Width / (float) imageSize.Width;
            return new Size(size.Width, this.Scale(imageSize.Height, scaleNumber1));
        }
        protected override string GetSource()
        {
            return source;
        }
        protected override bool IsBroken(ImageField field) => field != null && field.MediaItem == null;
        protected override void ParseField(ImageField imageFieldParse)
        {
            Assert.ArgumentNotNull((object) imageFieldParse, nameof(imageFieldParse));
            if (!string.IsNullOrEmpty(this.database)) imageFieldParse.MediaDatabase = Factory.GetDatabase(this.database);
            if (!string.IsNullOrEmpty(this.language)) imageFieldParse.MediaLanguage = Language.Parse(this.language);
            if (!string.IsNullOrEmpty(this.version)) imageFieldParse.MediaVersion = Sitecore.Data.Version.Parse(this.version);
            if (imageFieldParse.MediaItem != null) this.source = StringUtil.GetString(this.source, imageFieldParse.MediaItem.Paths.FullPath);
            this.alt = StringUtil.GetString(this.alt, imageFieldParse.Alt);
            this.border = StringUtil.GetString(this.border, imageFieldParse.Border);
            this.hspace = StringUtil.GetString(this.hspace, imageFieldParse.HSpace);
            this.vspace = StringUtil.GetString(this.vspace, imageFieldParse.VSpace);
            this.className = StringUtil.GetString(this.className, imageFieldParse.Class);
        }
        protected override void ParseNode(SafeDictionary < string > attributes)
        {
            Assert.ArgumentNotNull((object) attributes, nameof(attributes));
            string str = this.Extract(attributes, "outputMethod");
            this.xhtml = str == "xhtml" || Settings.Rendering.ImagesAsXhtml && str != "html";
            // this.source = this.Extract(attributes, "src");
            this.alt = this.Extract(attributes, "alt");
            this.border = this.Extract(attributes, "border");
            this.hspace = this.Extract(attributes, "hspace");
            this.vspace = this.Extract(attributes, "vspace");
            this.className = this.Extract(attributes, "class");
            if (string.IsNullOrEmpty(this.border) && !this.xhtml) this.border = "0";
            this.allowStretch = MainUtil.GetBool(this.Extract(attributes, ref this.asSet, "allowStretch", "as"), false);
            this.ignoreAspectRatio = MainUtil.GetBool(this.Extract(attributes, "ignoreAspectRatio", "iar"), false);
            this.width = MainUtil.GetInt(this.Extract(attributes, ref this.widthSet, "width", "w"), 0);
            this.height = MainUtil.GetInt(this.Extract(attributes, ref this.heightSet, "height", "h"), 0);
            this.scale = MainUtil.GetFloat(this.Extract(attributes, ref this.scaleSet, "scale", "sc"), 0.0 f);
            this.maxWidth = MainUtil.GetInt(this.Extract(attributes, ref this.maxWidthSet, "maxWidth", "mw"), 0);
            this.maxHeight = MainUtil.GetInt(this.Extract(attributes, ref this.maxHeightSet, "maxHeight", "mh"), 0);
            this.thumbnail = MainUtil.GetBool(this.Extract(attributes, ref this.thumbnailSet, "thumbnail", "thn"), false);
            this.backgroundColor = this.Extract(attributes, "backgroundColor", "bc") ?? string.Empty;
            this.database = this.Extract(attributes, "database", "db");
            this.language = this.Extract(attributes, "language", "la");
            this.version = this.Extract(attributes, "version", "vs");
            this.disableMediaCache = MainUtil.GetBool(this.Extract(attributes, ref this.disableMediaCacheSet, "disableMediaCache"), false);
        }
        protected override int Scale(int value, float scaleNumber) => (int) Math.Round((double) value * (double) scaleNumber);
        private string Extract(SafeDictionary < string > values, ref bool valueSet, params string[] keys)
        {
            Assert.ArgumentNotNull((object) values, nameof(values));
            Assert.ArgumentNotNull((object) keys, nameof(keys));
            string str = this.Extract(values, keys);
            valueSet = str != null;
            return str;
        }
    }
}

DAMs can be powerful tools for maintaining all content in a single repository. 


From here, we can create the new pipeline. What we will do is use our ExternalImageFieldRenderer to render the field. We pass it the source from our XML attribute value and then the field gets rendered with src="{ourExternalUrl}" on the page. 

namespace MySite.Pipelines.RenderField
{
    public class GetExternalUrlImageRenderField: GetImageFieldValue
    {
        private readonly User _currentUser;
        public GetExternalUrlImageRenderField() => this._currentUser = User.Current;
        internal GetExternalUrlImageRenderField(User currentUser) => this._currentUser = currentUser;
        public override void Process(RenderFieldArgs args)
        {
            if (!(args.FieldTypeKey.ToLower() == "image") || string.IsNullOrWhiteSpace(args.FieldValue)) return;
            XElement xelement = XElement.Parse(args.FieldValue);
            XAttribute xattribute1 = xelement.Attribute((XName) Constants.Fields.ExternalUrl);
            if (xattribute1 != null && !string.IsNullOrWhiteSpace(xattribute1.Value))
            {
                ImageRenderer renderer = this.CreateRenderer();
                var externalRenderer = GetExternalRenderer(args, renderer, xattribute1.Value);
                this.SetRenderFieldResult(externalRenderer.Render(), args);
            }
            else
            {
                base.Process(args);
            }
        }
        private ImageRenderer GetExternalRenderer(RenderFieldArgs args, ImageRenderer imageRenderer, string source)
        {
            var renderer = new ExternalImageRenderer()
            {
                Source = source
            };
            Item itemToRender = args.Item;
            renderer.Item = itemToRender;
            renderer.FieldName = args.FieldName;
            renderer.FieldValue = args.FieldValue;
            renderer.Parameters = args.Parameters;
            if (itemToRender == null) return renderer;
            renderer.Parameters.Add("la", itemToRender.Language.Name);
            this.EnsureMediaItemTitle(args, itemToRender, renderer);
            return renderer;
        }
    }
}

From there, just register the pipeline in a patch config and our field extension is done: 

<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="MySite.Pipelines.RenderField.GetExternalUrlImageRenderField, MySite"/> 
</renderField> 
</pipelines> 

Wrapping up 

So far, what we have done is: 

  • Created a new field type to store External URL 
  • Created a renderField pipeline to render the external URL for an image, rather than a Sitecore Media Item 

What we'll tackle in the next in the series: 

  • Creating a dialog to store the external URL on the field xml value for both: 
  • Content Editor 
  • Experience Editor 
  • Telling Sitecore how to display the image in content editor 

References: Sitecore and More: Extending an Image field in Sitecore (part 1)