Laptop with code

Implementing a Custom DAM Connector in Sitecore (Part 2)

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.   

In the previous post, we discussed extending the image field type, so that Sitecore can store an external URL and display that image during render. This post will discuss implementing a custom dialog in Sitecore so that content editors can select an image to be saved to the image field. 

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 

Implementing a Custom Dialog for a DAM 

The first step for developing a custom dialog for your DAM is to determine if your DAM has an API or universal connector you can use to fetch assets. In my scenario, the DAM vendor provides a universal Javascript connector that I was able to implement into Sitecore.  

This is where things will get more custom for you, so I won't go into major detail besides what Sitecore itself is looking for on image selection. 

Sending back the right data 

Your dialog will need to send the right data back to Sitecore on selection of the asset. That code is going to look something like this in your Speak dialog (or other Javascript file): 

var result = {
    source: asset.directUri,
    thumbnail: asset.previewUri,
    id: asset.id // You might not need this
};
var radWindow;
if (this.window.radWindow)
{
    radWindow = this.window.radWindow;
    radWindow.Close(result);
}
else if (this.window.frameElement && this.window.frameElement.radWindow)
{
    radWindow = this.window.frameElement.radWindow;
    radWindow.Close(result);
}
else
{
    var returnedResult = JSON.stringify(result);
    this.window.returnValue = returnedResult;
    this.window.top.returnValue = returnedResult;
    this.window.top.dialogClose();
}

The above code gets interpreted and called upon in one of two places. Either in content editor or in experience editor - you can call the same dialog you create for both. 

Interpreting the return in Content Editor 

We need to accomplish a few things before we call things done for the content editor side of this integration: 

  • Show the image in content editor 
  • Call our custom dialog 
  • Set values in the XML for the field so our previous field renderer knows how to render the image correctly 

See below for an annotated example implementation that extends the Sitecore.Shell.Applications.ContentEditor.Image class : 

namespace MySite.ContentEditor
    {
        public class ExternalUrlImage: Image
            {
                public ExternalUrlImage()
                {
                    this.Class = "scContentControlImage";
                    this.Change = "#";
                    this.Activation = true;
                }
                protected override void DoRender(HtmlTextWriter output)
                {
                    Assert.ArgumentNotNull((object) output, nameof(output));
                    if (!string.IsNullOrEmpty(this.XmlValue.GetAttribute(Constants.Fields.ExternalUrl)))
                    {
                        Sitecore.Data.Items.Item mediaItem = this.GetMediaItem();
                        string src;
                        this.GetThumbnailSrc(out src);
                        string str1 = " src=\"" + src + "\"";
                        string str2 = " id=\"" + this.ID + "_image\"";
                        string str3 = " alt=\"" + (mediaItem != null ? HttpUtility.HtmlEncode(mediaItem["Alt"]) : string.Empty) + "\"";
                        output.Write("<div id=\"" + this.ID + "_pane\" class=\"scContentControlImagePane\">");
                        string clientEvent = Sitecore.Context.ClientPage.GetClientEvent(this.ID + ".Browse");
                        output.Write("<div class=\"scContentControlImageImage\" onclick=\"" + clientEvent + "\">");
                        output.Write("<iframe" + str2 + str1 + str3 + " frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" width=\"100%\" height=\"128\" allowtransparency=\"allowtransparency\"></iframe>");
                        output.Write("</div>");
                        output.Write("<div id=\"" + this.ID + "_details\" class=\"scContentControlImageDetails\">");
                        string details = this.GetDetails();
                        output.Write(details);
                        output.Write("</div>");
                        output.Write("</div>");
                    }
                    else
                    {
                        base.DoRender(output);
                    }
                }

The above code renders the custom external URL image within content editor. Note that it uses our thumbnail source, which we will set later from our DAM. 

private void GetSrc(out string src)
{
    src = string.Empty;
    if (!string.IsNullOrEmpty(this.XmlValue.GetAttribute(Constants.Fields.ExternalUrl)))
    {
        src = this.XmlValue.GetAttribute(Constants.Fields.ExternalUrl);
    }
    else
    {
        MediaItem mediaItem = (MediaItem) this.GetMediaItem();
        if (mediaItem == null) return;
        MediaUrlBuilderOptions thumbnailOptions = MediaUrlBuilderOptions.GetThumbnailOptions(mediaItem);
        int result;
        if (!int.TryParse(mediaItem.InnerItem["Height"], out result)) result = 128;
        thumbnailOptions.Height = new int ? (Math.Min(128, result));
        thumbnailOptions.MaxWidth = new int ? (640);
        thumbnailOptions.UseDefaultIcon = new bool ? (true);
        src = MediaManager.GetMediaUrl(mediaItem, thumbnailOptions);
    }
}
protected void ExternalUpdate()
{
    SheerResponse.Eval("$('" + this.ID + "_image').src = \"" + this.XmlValue.GetAttribute(Constants.Fields.ThumbnailUrl) + "\"");
    SheerResponse.SetInnerHtml(this.ID + "_details", this.GetDetails());
    SheerResponse.Eval("scContent.startValidators()");
}
private void GetThumbnailSrc(out string src)
{
    src = string.Empty;
    if (!string.IsNullOrEmpty(this.XmlValue.GetAttribute(Constants.Fields.ThumbnailUrl)))
    {
        src = this.XmlValue.GetAttribute(Constants.Fields.ThumbnailUrl);
    }
    else
    {
        MediaItem mediaItem = (MediaItem) this.GetMediaItem();
        if (mediaItem == null) return;
        MediaUrlBuilderOptions thumbnailOptions = MediaUrlBuilderOptions.GetThumbnailOptions(mediaItem);
        int result;
        if (!int.TryParse(mediaItem.InnerItem["Height"], out result)) result = 128;
        thumbnailOptions.Height = new int ? (Math.Min(128, result));
        thumbnailOptions.MaxWidth = new int ? (640);
        thumbnailOptions.UseDefaultIcon = new bool ? (true);
        src = MediaManager.GetMediaUrl(mediaItem, thumbnailOptions);
    }
}

The above methods are getting the source for the image as the external URL/thumbnail source instead of from a media item. We need to override these methods so the image is sourced properly. 

private string GetDetails()
{
    string details = string.Empty;
    if (!string.IsNullOrEmpty(this.XmlValue.GetAttribute(Constants.Fields.ExternalUrl)))
    {
        StringBuilder stringBuilder = new StringBuilder();
        XmlValue xmlValue = this.XmlValue;
        stringBuilder.Append("<div>");
        string externalLink = HttpUtility.HtmlEncode(xmlValue.GetAttribute(Constants.Fields.ExternalUrl));
        stringBuilder.Append(string.Format("External Link Url : {0}", externalLink));
        stringBuilder.Append("</div>");
        details = stringBuilder.ToString();
    }
    else
    {
        MediaItem mediaItem = (MediaItem) this.GetMediaItem();
        if (mediaItem != null)
        {
            Sitecore.Data.Items.Item innerItem = mediaItem.InnerItem;
            StringBuilder stringBuilder = new StringBuilder();
            XmlValue xmlValue = this.XmlValue;
            stringBuilder.Append("<div>");
            string str1 = innerItem["Dimensions"];
            string str2 = HttpUtility.HtmlEncode(xmlValue.GetAttribute("width"));
            string str3 = HttpUtility.HtmlEncode(xmlValue.GetAttribute("height"));
            if (!string.IsNullOrEmpty(str2) || !string.IsNullOrEmpty(str3)) stringBuilder.Append(Translate.Text("Dimensions: {0} x {1} (Original: {2})", (object) str2, (object) str3, (object) str1));
            else stringBuilder.Append(Translate.Text("Dimensions: {0}", (object) str1));
            stringBuilder.Append("</div>");
            stringBuilder.Append("<div style=\"padding:2px 0px 0px 0px\">");
            string str4 = HttpUtility.HtmlEncode(innerItem["Alt"]);
            string str5 = HttpUtility.HtmlEncode(xmlValue.GetAttribute("alt"));
            if (!string.IsNullOrEmpty(str5) && !string.IsNullOrEmpty(str4)) stringBuilder.Append(Translate.Text("Alternate Text: \"{0}\" (Default Alternate Text: \"{1}\")", (object) str5, (object) str4));
            else if (!string.IsNullOrEmpty(str5)) stringBuilder.Append(Translate.Text("Alternate Text: \"{0}\"", (object) str5));
            else if (!string.IsNullOrEmpty(str4)) stringBuilder.Append(Translate.Text("Default Alternate Text: \"{0}\"", (object) str4));
            else stringBuilder.Append(Translate.Text("Warning: Alternate Text is missing."));
            stringBuilder.Append("</div>");
            details = stringBuilder.ToString();
        }
    }
    if (details.Length == 0) details = Translate.Text("This media item has no details.");
    return details;
}

This method gives tooltip information during rendering in content editor for the author - in this case, if we have an external URL, show the author what URL they are referencing. 

public override void HandleMessage(Message message)
{
    if (message.Name == "contentimage:clear" && string.IsNullOrEmpty(this.Value) && !string.IsNullOrEmpty(this.XmlValue.GetAttribute(Constants.Fields.ExternalUrl)) && !string.IsNullOrEmpty(this.XmlValue.GetAttribute("src"))) this.Value = this.XmlValue.GetAttribute("src");
    base.HandleMessage(message);
    if (message.Name == "contentimage:refresh")
        if (!string.IsNullOrEmpty(this.XmlValue.GetAttribute(Constants.Fields.ExternalUrl))) ExternalUpdate();
        else if (!string.IsNullOrEmpty(this.XmlValue.GetAttribute("src"))) this.Update();
    if (!(message["id"] == this.ID) || !(message.Name == "dam_contentimage:open")) return;
    Sitecore.Context.ClientPage.Start((object) this, "BrowseDAMImage");
}
protected void BrowseDAMImage(ClientPipelineArgs args)
{
    if (args.IsPostBack)
    {
        if (string.IsNullOrWhiteSpace(args.Result) || !(args.Result != "undefined")) return;
        var resultObj = JsonConvert.DeserializeObject < ExternalUrlImageModel > (args.Result);
        XmlValue.SetAttribute(Constants.Fields.ThumbnailUrl, resultObj.Thumbnail);
        XmlValue.SetAttribute(Constants.Fields.DAMId, resultObj.Id);
        XmlValue.SetAttribute(Constants.Fields.ExternalUrl, CantoHelper.GetExternalUrlFromId(resultObj.Id));
        ExternalUpdate();
        this.SetModified();
        SheerResponse.Refresh(this);
    }
    else
    {
        SheerResponse.ShowModalDialog("/sitecore/shell/client/Applications/DAMApp", "1100px", "669px", string.Empty, true);
        args.WaitForPostBack();
    }
}

This is how everything ties together. When Sitecore receives a message that we want to browse our DAM image, it launches the DAMApp that we created with the Javascript code earlier. Note that this can just be an index.aspx page at the file path /sitecore/shell/client/Applications/DAMApp folder. It's certainly easier to set it up that way than as a Sitecore SPEAK page, but it is possible. When the application/connector sends back data, the JSON is deserialized and then we set the values on the XML raw value of the image. Any constants are fine here, as long as they don't overlap with reserved ones in Sitecore (and they are the same ones you are using for rendering). 

protected new void ShowProperties(ClientPipelineArgs args)
{
    if (!string.IsNullOrWhiteSpace(this.XmlValue.GetAttribute(Constants.Fields.ExternalUrl)))
    {
        if (!args.IsPostBack)
        {
            UrlString urlString = new UrlString(FileUtil.MakePath("/sitecore/shell", ControlManager.GetControlUrl(new ControlName("Sitecore.Shell.Applications.Media.ImageProperties"))));
            urlString.Add("id", "{11111111-1111-1111-1111-111111111111}");
            UrlHandle urlHandle = new UrlHandle();
            XmlValue xmlValue = new XmlValue(this.XmlValue.ToString(), "image");
            xmlValue.SetAttribute("mediaid", "{11111111-1111-1111-1111-111111111111}");
            urlHandle["xmlvalue"] = xmlValue.ToString();
            urlHandle.Add(urlString);
            SheerResponse.ShowModalDialog(urlString.ToString(), true);
            args.WaitForPostBack();
        }
        else
        {
            if (!args.HasResult) return;
            XmlValue xmlValue = new XmlValue(args.Result, "image");
            string attribute1 = xmlValue.GetAttribute("alt");
            string attribute2 = xmlValue.GetAttribute("height");
            string attribute3 = xmlValue.GetAttribute("width");
            string attribute4 = xmlValue.GetAttribute("vspace");
            string attribute5 = xmlValue.GetAttribute("hspace");
            this.XmlValue.SetAttribute("alt", attribute1);
            this.XmlValue.SetAttribute("height", attribute2);
            this.XmlValue.SetAttribute("width", attribute3);
            this.XmlValue.SetAttribute("vspace", attribute4);
            this.XmlValue.SetAttribute("hspace", attribute5);
            this.SetModified();
            ExternalUpdate();
        }
    }
    else base.ShowProperties(args);
}
}

We also have to override the properties dialog for Sitecore here. This lets us do things like set alt text on our DAM images. The above code mostly just creates a mocked up media item so Sitecore is happy with setting alt text on the content item being edited. 

You will need to register this so Sitecore can see it: 

<controlSources> 
     <source mode="on" namespace="MySite.ContentEditor" assembly="MySite" prefix="extended"/> 
</controlSources> 

Set the "Control" field on the /sitecore/system/Field types/Simple Types/Image item in the core database equal to "extended:ExternalUrlImage": 

Then, to have your dialog work, add a new item under /sitecore/system/Field types/Simple Types/Image/Menu in the core database.

Set the message to the message you are interpreting in your Handle Message.

This should wrap up things for the content editor side of things.

Interpreting the return in Experience Editor

The code for experience editor works much the same as content editor, but it doesn't need to interpret as many things (since the image is just displayed on the page instead of in a separate box in content).

namespace MySite.Commands
{
    public class DAMImageCommand: ChooseImage
    {
        protected static void Run(ClientPipelineArgs args)
        {
            if (args.IsPostBack)
            {
                if (string.IsNullOrWhiteSpace(args.Result) || !(args.Result != "undefined")) return;
                Item itemNotNull = Client.GetItemNotNull(args.Parameters["itemid"], Language.Parse(args.Parameters["language"]));
                itemNotNull.Fields.ReadAll();
                Field field = itemNotNull.Fields[args.Parameters["fieldid"]];
                string parameter = args.Parameters["controlid"];
                ImageField imageField = new ImageField(field, string.Empty);
                var resultObj = JsonConvert.DeserializeObject < ExternalUrlImageModel > (args.Result);
                imageField.SetAttribute(Constants.Fields.ThumbnailUrl, resultObj.Thumbnail);
                imageField.SetAttribute(Constants.Fields.CantoId, resultObj.Id);
                imageField.SetAttribute(Constants.Fields.ExternalUrl, CantoHelper.GetExternalUrlFromId(resultObj.Id));
                var imageValue = WebEditImageCommand.RenderImage(args, imageField.Value);
                SheerResponse.SetAttribute("scHtmlValue", "value", imageValue);
                SheerResponse.SetAttribute("scPlainValue", "value", imageField.Value);
                SheerResponse.Eval("scSetHtmlValue('" + parameter + "')");
            }
            else
            {
                SheerResponse.ShowModalDialog("/sitecore/shell/client/Applications/DAMApp", "1200px", "700px", string.Empty, true);
                args.WaitForPostBack();
            }
        }
    }
}

This does the same thing as our BrowseDAMImage in Content Editor, but that is all we need for the command in Experience Editor.

Register the command in a patch config:

<sitecore role="Standalone or ContentManagement">
	<commands>
		<command name="webedit:chooseDAMImage" type="MySite.Commands.DAMImageCommand, MySite" />
	</commands>
</sitecore>

After that, create a new Web Edit button under:

/sitecore/system/Field types/Simple Types/Image/WebEdit Buttons

This should be all you need in order to get this working in both Experience editor and Content editor.

Wrapping Up

What we have done in this post is:

  • Provided guidelines for sending back information from your DAM to Sitecore to store values in raw XML for external URLs using a custom dialog
  • Created an image control for external URLs for Content editor
  • Create a command for users in Experience editor to select images from a DAM and store values

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

  • Serving our external (DAM) images optimized, for when a DAM is unable to serve in next gen formats, or DAM asset uploaders do not upload optimized images
Found this article interesting? Chat with our account specialists to get started on your next digital journey.