Extend general link field in Sitecore

In this two part series I’ll be covering how to extend the general link field and the rich text hyperlink dialog in Sitecore. Recently a client requested an interesting feature. They have an external system that exposes an api that they want to query to get document names and links. They want to be able to run those queries in real time and set hyperlinks based on the a selected item. Here is what UI looks like when it’s done. A new button is added and a custom dialog is shown when clicked. For more reading before you proceed, check out this link on xmlcontrols. https://doc.sitecore.com/SdnArchive/Articles/XML%20Sheer%20UI/Beginning%20with%20XML%20controls.html

Let’s dive into updating the content editor general link field first.

Update the core database

Head over to the core database and find the field type of “general link” field in this path: /sitecore/system/Field types/Link Types/General Link

We need to do a few things here. Add a new menu item and name it “Search” and update the Display name and message. After “contentlink:” you can add your custom message name here. In my case, I used “searchxyzsystem”.

Then click on the “General Link” item and update the “Control” field. Originally, it says “content:Link”, but we are changing that to be “content:CustomLinks”. This field originally points to the “Link” control class in Sitecore.Shell.Applications.ContentEditor.Link. We are going to make a new class and inherit from the “Link” class, so let’s give the new class a name of “CustomLinks.” If you decide to add more options to the general link field, you can modify this class moving forward.

Create the message handler

Open the Sitecore.Kernel in a decompiler of your choice and head over to the Sitecore.Shell.Applications.ContentEditor.Link class.

If you take a close look at it, you’ll see that we can override the “HandleMessage” method. This is great. We can now create that class that Sitecore will call first, that will inherit from Link and handle our new “message”. Here is the class. We called it “CustomLinks”

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Text;
using Sitecore.Web.UI.Sheer;

namespace SampleSite.ControlSources
{
    public class CustomLinks : Link
    {
        public override void HandleMessage(Message message)
        {
            Assert.ArgumentNotNull((object)message, nameof(message));
            base.HandleMessage(message);
            if (message["id"] != this.ID)
                return;
            string name = message.Name;

            switch (name)
            {
                case "contentlink:searchxyzsystem":
                    {
                        string uri = UIUtil.GetUri("control:SearchXYZSystemLinkForm");
                        UrlString urlString = new UrlString(uri);
                        string strUrlString = urlString.ToString();
                        this.Insert(strUrlString);
                        break;
                    }
            }
        }
    }
}

We need to tell Sitecore where to find this class. Register the control source with Sitecore. If you open the ShowConfig.aspx and do a search for “ControlSources” you’ll find an element that has all of the OOTB control sources defined.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:security="http://www.sitecore.net/xmlconfig/security/">
  <sitecore>
    <controlSources>
      <source assembly="SampleSite" namespace="SampleSite.ControlSources" mode="on" prefix="content"/>
    </controlSources>
  </sitecore>
</configuration>

We are overriding the “HandleMessage” method to add in an additional message.Name case statement. In our case, we are looking for what we added in Sitecore, “contentlink:searchxyzsystem”. Look at the “this.Insert” method. In the decompiled code, Sitecore is passing in urls directly to embeded aspx controls. In our case, to extend Sitecore, we are making a new control that is not embeded.

Creating the control

To do this, we need to do 2 things, create the xml for the xaml control and create a class called “SearchXYZSystemLinkForm”. This class is what Sitecore is looking for in our HandleMessage method above.

Here is the code for the .cs file.

using SampleSite.ServiceLayer.XYZPlatform;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.Dialogs;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Xml;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SampleSite.ControlSources
{
    public class SearchXYZSystemLinkForm : LinkForm
    {
        protected Listview ItemList;
        protected Edit Anchor;
        protected Edit Class;
        protected Panel CustomLabel;
        protected Edit CustomTarget;
        protected Combobox Target;
        protected Edit Text;
        protected Edit Title;
        protected Edit Url;
        protected string Term;
        protected Literal NumberOfItems;
        protected Edit SearchTerm;

        public SearchXYZSystemLinkForm()
        {
        }
        protected void OnListboxChanged()
        {
            if (this.Target.Value == "Custom")
            {
                this.CustomTarget.Disabled = false;
                this.CustomLabel.Disabled = false;
            }
            else
            {
                this.CustomTarget.Value = string.Empty;
                this.CustomTarget.Disabled = true;
                this.CustomLabel.Disabled = true;
            }
        }

        protected void FillItemList(List<XYZPlatformSearchResult> searchResults)
        {
            if (!searchResults.Any())
            {
                return;
            }

            foreach (XYZPlatformSearchResult document in searchResults)
            {
                // Create and add the new ListviewItem control to the Listview.
                // We have to assign an unique ID to each control on the page.
                ListviewItem listItem = new ListviewItem();
                Context.ClientPage.AddControl(ItemList, listItem);
                listItem.ID = Control.GetUniqueID("I");

                // Populate the list item with data.
                listItem.Icon = "Applications/16x16/document_view.png";
                listItem.Header = document.Name;
                listItem.ColumnValues["name"] = document.Name;
                listItem.ColumnValues["url"] = document.Url;
            }

            // Set status bar text
            if (searchResults.Count != 1)
            {
                NumberOfItems.Text = searchResults.Count.ToString() + " items";
            }
            else
            {
                NumberOfItems.Text = "1 item";
            }
        }

        protected void Search()
        {
            ItemList.Controls.Clear();
            string searchTerm = base.LinkAttributes["SearchTerm"];
            List<XYZPlatformSearchResult> searchResults = XYZPlatformService.Search(searchTerm);
            FillItemList(searchResults);

            // We need to replace the html in order to avoid duplicate ID's
            Context.ClientPage.ClientResponse.SetOuterHtml("ItemList", ItemList);
        }

        protected void ItemList_SelectionChanged()
        {
            if (ItemList.SelectedItems.Length != 1)
            {
                return;
            }

            // Retrieve the item selected in the listview
            string selected = ItemList.SelectedItems[0].ColumnValues["url"] as string;
            this.Url.Value = selected;
        }

        protected override void OnLoad(EventArgs e)
        {
            Assert.ArgumentNotNull((object)e, nameof(e));         
            base.OnLoad(e);
            if (Context.ClientPage.IsEvent)
                return;
            string str1 = this.LinkAttributes["url"];
            if (this.LinkType != "searchxyz")
                str1 = string.Empty;
            string str2 = string.Empty;
            string linkAttribute = this.LinkAttributes["target"];     
            string linkTargetValue = LinkForm.GetLinkTargetValue(linkAttribute);
            if (linkTargetValue == "Custom")
            {
                str2 = linkAttribute;
                this.CustomTarget.Disabled = false;
                this.CustomLabel.Disabled = false;
            }
            this.Text.Value = this.LinkAttributes["text"];
            this.Url.Value = str1;
            this.Target.Value = linkTargetValue;
            this.CustomTarget.Value = str2;
            this.Class.Value = this.LinkAttributes["class"];
            this.Title.Value = this.LinkAttributes["title"];
        }

        protected override void OnOK(object sender, EventArgs args)
        {
            Assert.ArgumentNotNull(sender, nameof(sender));
            Assert.ArgumentNotNull((object)args, nameof(args));
            string attributeFromValue = LinkForm.GetLinkTargetAttributeFromValue(this.Target.Value, this.CustomTarget.Value);
            Packet packet = new Packet("link", Array.Empty<string>());
            LinkForm.SetAttribute(packet, "text", (Control)this.Text);
            LinkForm.SetAttribute(packet, "linktype", "searchxyz");
            LinkForm.SetAttribute(packet, "url", this.Url);
            LinkForm.SetAttribute(packet, "anchor", string.Empty);
            LinkForm.SetAttribute(packet, "title", (Control)this.Title);
            LinkForm.SetAttribute(packet, "class", (Control)this.Class);
            LinkForm.SetAttribute(packet, "target", attributeFromValue);
            Context.ClientPage.ClientResponse.SetDialogValue(packet.OuterXml);
            base.OnOK(sender, args);
        }
    }
}

Here is the code for the .xml xaml control. A couple of things to note here. Pay attention to the node name of the first child of the control element. Also, the “CodeBeside” node should point to the class you made above.

 <?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
  <SearchXYZSystemLinkForm>
    <FormDialog Icon="Applications/16x16/document_view.png" Header="Search For External Document" Text="Search For External Document" OKButton="OK">
      <CodeBeside Type="SampleSite.ControlSources.SearchXYZSystemLinkForm, SampleSite"/>
      <!-- Search Panel -->
      <GridPanel Class="scFormTable" CellPadding="2" Columns="1" Width="100%">
        <Label For="SearchTerm" GridPanel.NoWrap="false">
          <Literal Text="Enter Search Term:"/>
        </Label>
        <Edit ID="SearchTerm" Width="100%" GridPanel.Width="70%"/>
        <Button ID="Search" Click="Search" Header="Search"></Button>
      </GridPanel>

      <!-- Search Panel -->
      <GridPanel Class="scFormTable" CellPadding="2" Columns="1" Width="100%">
        <Literal Text="Double click an item to select the url." Padding="2" />
      </GridPanel>

      <!-- Search Results -->
      <GridPanel Class="scFormTable" CellPadding="2" Columns="1" Width="100%">
        <Scrollbox Width="100%" Height="100%" Padding="0">
          <Listview ID="ItemList" View="Details" Width="100%" Background="white" DblClick="ItemList_SelectionChanged">
            <ListviewHeader>
              <ListviewHeaderItem Name="name" Header="Name" />
              <ListviewHeaderItem Name="url" Header="Url" />
            </ListviewHeader>
          </Listview>
        </Scrollbox>
      </GridPanel>
      
      <!-- Status bar region -->
      <Border Height="22" GridPanel.Height="22" Background="#e9e9e9">
        <GridPanel Columns="2" Width="100%" Height="100%" CellPadding="2">
          <Border Border="1px inset" Padding="2" Height="100%">
            <Literal ID="NumberOfItems"/>
          </Border>
        </GridPanel>
      </Border>
      
      <!-- Create some space -->
      <Border Height="22" GridPanel.Height="22">
        <GridPanel Columns="2" Width="100%" Height="100%" CellPadding="2">
        </GridPanel>
      </Border>

      <!-- Link Properties -->
      <GridPanel Class="scFormTable" CellPadding="2" Columns="2" Width="100%">
        <Label For="Text" GridPanel.NoWrap="true">
          <Literal Text="Link description:"/>
        </Label>
        <Edit ID="Text" Width="100%" GridPanel.Width="100%"/>

        <Label For="Url" GridPanel.NoWrap="true">
          <Literal Text="Document Url:"/>
        </Label>
        <Edit ID="Url" Width="100%" GridPanel.Width="100%" />
        
        <Label for="Target" GridPanel.NoWrap="true">
          <Literal Text="Target window:"/>
        </Label>
        <Combobox ID="Target" GridPanel.Width="100%" Width="100%" Change="OnListboxChanged">
          <ListItem Value="Self" Header="Active browser"/>
          <ListItem Value="Custom" Header="Custom"/>
          <ListItem Value="New" Header="New browser"/>
        </Combobox>

        <Panel ID="CustomLabel" Disabled="true" Background="transparent" Border="none" GridPanel.NoWrap="true">
          <Label For="CustomTarget">
            <Literal Text="Custom:" />
          </Label>
        </Panel>
        <Edit ID="CustomTarget" Width="100%" Disabled="true"/>

        <Label For="Class" GridPanel.NoWrap="true">
          <Literal Text="Style class:" />
        </Label>
        <Edit ID="Class" Width="100%" />

        <Label for="Title" GridPanel.NoWrap="true">
          <Literal Text="Alternate text:"/>
        </Label>
        <Edit ID="Title" Width="100%" />
      </GridPanel>
    </FormDialog>
  </SearchXYZSystemLinkForm>
</control>

To make this easier for you, these are the locations of where I placed all these files:

SampleSite\ControlSources\CustomLinks.cs
SampleSite\ControlSources\SearchXYZSystemLinkForm.cs
SampleSite\App_Config\Environment\SampleSite.ControlSoures.config
SampleSite\sitecore\shell\applications\dialogs\SearchXYZSystemLink\SearchXYZSystemLink.xml

That’s really it to create the new Dialog and integrate it with Sitecore. The control uses XAML and is generally pretty easy to work with. The other code to make this happen for our use case is the service layer and model. I’ve created a class that returns mocked data for testing purposes.

namespace SampleSite.ServiceLayer.XYZPlatform
{
    public class XYZPlatformSearchResult
    {
        public string Name { get; set; }
        public string Url { get; set; }

    }
}
using System.Collections.Generic;

namespace SampleSite.ServiceLayer.XYZPlatform
{
    public class XYZPlatformService
    {
        public static List<XYZPlatformSearchResult> Search(string term)
        {
            List<XYZPlatformSearchResult> results = new List<XYZPlatformSearchResult>();

            XYZPlatformSearchResult r1 = new XYZPlatformSearchResult() { Name = "Alpha Romeo", Url = "https://www.alfaromeo.com/" };  
            XYZPlatformSearchResult r2 = new XYZPlatformSearchResult() { Name = "Aston Martin", Url = "https://www.astonmartin.com/en-us/" };
            XYZPlatformSearchResult r3 = new XYZPlatformSearchResult() { Name = "Audi", Url = "https://www.audi.co.uk/" };
            XYZPlatformSearchResult r4 = new XYZPlatformSearchResult() { Name = "BMW", Url = "https://www.bmw.co.uk/en/index.html" };
            XYZPlatformSearchResult r5 = new XYZPlatformSearchResult() { Name = "Caterham", Url = "https://www.caterhamcars.com/en" };
            XYZPlatformSearchResult r6 = new XYZPlatformSearchResult() { Name = "Chevrolet", Url = "https://www.chevrolet.co.uk/" };
            XYZPlatformSearchResult r7 = new XYZPlatformSearchResult() { Name = "Citroen", Url = "https://www.citroen.co.uk/" };
            XYZPlatformSearchResult r8 = new XYZPlatformSearchResult() { Name = "Ferrari", Url = "https://www.ferrari.com/en-US" };
            XYZPlatformSearchResult r9 = new XYZPlatformSearchResult() { Name = "Fiat", Url = "https://www.fiat.co.uk/" };
            XYZPlatformSearchResult r10 = new XYZPlatformSearchResult() { Name = "Ford", Url = "https://www.ford.co.uk/" };
            XYZPlatformSearchResult r11 = new XYZPlatformSearchResult() { Name = "Honda", Url = "https://www.honda.co.uk/" };
            XYZPlatformSearchResult r12 = new XYZPlatformSearchResult() { Name = "Hyundai", Url = "https://www.hyundai.co.uk/" };
            XYZPlatformSearchResult r13 = new XYZPlatformSearchResult() { Name = "Isuzu", Url = "https://www.isuzu.co.uk/" };
            XYZPlatformSearchResult r14 = new XYZPlatformSearchResult() { Name = "Jaguar", Url = "https://www.jaguar.com/market-selector.html" };
            XYZPlatformSearchResult r15 = new XYZPlatformSearchResult() { Name = "Landrover", Url = "https://www.landrover.co.uk/index.html" };
            XYZPlatformSearchResult r16 = new XYZPlatformSearchResult() { Name = "Mazda", Url = "https://www.mazda.co.uk/" };

            results.Add(r1);
            results.Add(r2);
            results.Add(r3);
            results.Add(r4);
            results.Add(r5);
            results.Add(r6);
            results.Add(r7);
            results.Add(r8);
            results.Add(r9);
            results.Add(r10);
            results.Add(r11);
            results.Add(r12);
            results.Add(r13);
            results.Add(r14);
            results.Add(r15);
            results.Add(r16);
            
            return results;

        }
    }
}

In part 2 of this series we’ll go over how to update the javascript dialog box that is part of the rich text editor.

About Phil Paris

Hi, my name is Phil Paris and I’m a Sitecore Architect and general Sitecore enthusiast. I’ve been working with Sitecore since 2013. Through this blog I will be sharing Sitecore insights, tips and tricks, best practices and general knowledge with the hopes to further the community at large. Please feel free to reach out to me at any time!

View all posts by Phil Paris →