MultilistField in Kommunikation mit WebServices

Die Datenfelder von Sitecore besitzen stets einen Fokus: Items. Nur was ist, wenn ich auf Funktionen eines weiteren Dienstes zugreifen möchte, ohne dies als Itemstruktur im Sitecore zu besitzen?

Dieser Wunschgedanke kommt je nach Anforderung schnell auf. Beispielsweise wenn ich ein explizites YouTube Video aus meinem Channel selektieren möchte oder auf nicht redaktionelle Listen, welche ich später im Code weiterverarbeiten möchte. Die Grenzen hierfür sind unendlich und gehen von einer einfachen XML-Datei bis hin zu SOAP, REST-Services oder andere properitäre Schnittstellen, welche man anbinden möchte.

Das Beispiel – FreeBase
Beispiel-Request auf den Freebase-Service und somit unsere Vorlage ist dieser HTTP-GET: https://www.googleapis.com/freebase/v1/search?query=sitecore&indent=true welches Freebase-Daten im JSON-Format zurück gibt.

Als kleiner Beispielcode, wie das in Sitecore gemacht werden könnte, habe ich mir das MultilistEx-Control zur Brust genommen und entsprechend eine Freebase-Service-Integration hinzugefügt.

Die Basisklasse für das kommende Handling von Field-Source, Selected- und Unselected-Items:

namespace Namics.Sample.CustomControls
{
    using System.Collections.Generic;
    using System.Web.UI;
    using Sitecore;
    using Sitecore.Diagnostics;
    using Sitecore.Globalization;
    using Sitecore.Resources;
    using Sitecore.Shell.Applications.ContentEditor;

    public abstract class MultilistExBase : MultilistEx
    {
        #region Methods

        /// <summary>
        /// The do render.
        /// </summary>
        /// <param name="pOutput">
        /// The output.
        /// </param>
        protected override void DoRender(HtmlTextWriter pOutput)
        {
            Assert.ArgumentNotNull(pOutput, "pOutput");

            ServerProperties["ID"] = ID;

            ManageSource();

            string text = string.Empty;
            if (ReadOnly)
            {
                text = " disabled=\"disabled\"";
            }

            pOutput.Write(string.Concat(new[] { "<input id=\"", ID, "_Value\" type=\"hidden\" value=\"", StringUtil.EscapeQuote(Value), "\" />" }));
            pOutput.Write("<table" + GetControlAttributes() + ">");
            pOutput.Write("<tr>");
            pOutput.Write("<td class=\"scContentControlMultilistCaption\" width=\"50%\">" + Translate.Text("All") + "</td>");
            pOutput.Write("<td width=\"20\">" + Images.GetSpacer(20, 1) + "</td>");
            pOutput.Write("<td class=\"scContentControlMultilistCaption\" width=\"50%\">" + Translate.Text("Selected") + "</td>");
            pOutput.Write("<td width=\"20\">" + Images.GetSpacer(20, 1) + "</td>");
            pOutput.Write("</tr>");
            pOutput.Write("<tr>");
            pOutput.Write("<td valign=\"top\" height=\"100%\">");
            pOutput.Write(
                string.Concat(
                    new[]
                        {
                            "<select id=\"", ID, "_unselected\" class=\"scContentControlMultilistBox\" multiple=\"multiple\" size=\"10\"", text,
                            " ondblclick=\"javascript:scContent.multilistMoveRight('", ID, "')\" onchange=\"javascript:document.getElementById('", ID,
                            "_all_help').innerHTML=this.selectedIndex>=0?this.options[this.selectedIndex].innerHTML:''\" >"
                        }));

            // Bind the selected values?
            foreach (KeyValuePair<string, string> field in GetNonSelectedItems())
            {
                pOutput.Write(string.Concat(new[] { "<option value=\"", field.Key, "\">", field.Value, "</option>" }));
            }

            pOutput.Write("</select>");
            pOutput.Write("</td>");
            pOutput.Write("<td valign=\"top\">");
            RenderButton(pOutput, "Core/16x16/arrow_blue_right.png", "javascript:scContent.multilistMoveRight('" + ID + "')");
            pOutput.Write(string.Empty);
            RenderButton(pOutput, "Core/16x16/arrow_blue_left.png", "javascript:scContent.multilistMoveLeft('" + ID + "')");
            pOutput.Write("</td>");
            pOutput.Write("<td valign=\"top\" height=\"100%\">");
            pOutput.Write(
                string.Concat(
                    new[]
                        {
                            "<select id=\"", ID, "_selected\" class=\"scContentControlMultilistBox\" multiple=\"multiple\" size=\"10\"", text,
                            " ondblclick=\"javascript:scContent.multilistMoveLeft('", ID, "')\" onchange=\"javascript:document.getElementById('", ID,
                            "_selected_help').innerHTML=this.selectedIndex>=0?this.options[this.selectedIndex].innerHTML:''\">"
                        }));

            // Bind the available items list
            foreach (KeyValuePair<string, string> field in GetSelectedItems())
            {
                pOutput.Write(string.Concat(new[] { "<option value=\"", field.Key, "\">", field.Value, "</option>" }));
            }

            pOutput.Write("</select>");
            pOutput.Write("</td>");
            pOutput.Write("<td valign=\"top\">");
            RenderButton(pOutput, "Core/16x16/arrow_blue_up.png", "javascript:scContent.multilistMoveUp('" + ID + "')");
            pOutput.Write(string.Empty);
            RenderButton(pOutput, "Core/16x16/arrow_blue_down.png", "javascript:scContent.multilistMoveDown('" + ID + "')");
            pOutput.Write("</td>");
            pOutput.Write("</tr>");
            pOutput.Write("<tr>");
            pOutput.Write("<td valign=\"top\">");
            pOutput.Write(
                "<div style=\"border:1px solid #999999;font:8pt tahoma;padding:2px;margin:4px 0px 4px 0px;height:14px\" id=\"" + ID
                + "_all_help\"></div>");
            pOutput.Write("</td>");
            pOutput.Write("<td></td>");
            pOutput.Write("<td valign=\"top\">");
            pOutput.Write(
                "<div style=\"border:1px solid #999999;font:8pt tahoma;padding:2px;margin:4px 0px 4px 0px;height:14px\" id=\"" + ID
                + "_selected_help\"></div>");
            pOutput.Write("</td>");
            pOutput.Write("<td></td>");
            pOutput.Write("</tr>");
            pOutput.Write("</table>");
        }

        /// <summary>
        ///   Return here your unselected items. First value is the ID you will store into your field, the second one is the display text.
        /// </summary>
        /// <returns> The unselected items </returns>
        protected abstract IEnumerable<KeyValuePair<string, string>> GetNonSelectedItems();

        /// <summary>
        ///   Return here your selected items. First value is the ID you will store into your field, the second one is the display text.
        /// </summary>
        /// <returns> The selected items </returns>
        protected abstract IEnumerable<KeyValuePair<string, string>> GetSelectedItems();

        /// <summary>
        ///   By overideing this method, you can initialise some variables here.
        /// </summary>
        protected abstract void ManageSource();
       
        /// <summary>
        /// The render button.
        /// </summary>
        /// <param name="pOutput">
        /// The output.
        /// </param>
        /// <param name="pIcon">
        /// The icon.
        /// </param>
        /// <param name="pClick">
        /// The click.
        /// </param>
        private void RenderButton(HtmlTextWriter pOutput, string pIcon, string pClick)
        {
            Assert.ArgumentNotNull(pOutput, "pOutput");
            Assert.ArgumentNotNull(pIcon, "pIcon");
            Assert.ArgumentNotNull(pClick, "pClick");
            var imageBuilder = new ImageBuilder { Src = pIcon, Width = 16, Height = 16, Margin = "2px" };
            if (!ReadOnly)
            {
                imageBuilder.OnClick = pClick;
            }

            pOutput.Write(imageBuilder.ToString());
        }

        #endregion
    }
}

Hierfür empfehle ich, Custom-Controls in ein separates VisualStudio-Projekt abzuspeichern. So können zusätzliche Controls später einfacher als Einzelnes oder ganzes Set im Sitecore aktiviert bzw. wieder deaktiviert werden.

FreeBase Control
Weiter hier die konkrete Klasse, welches direkt auf den Freebase-Webservice zugreift:

namespace Namics.Sample.CustomControls
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;

    using Newtonsoft.Json;

    public class FreeBaseMultilist : MultilistExBase
    {
        #region Public Properties

        /// <summary>
        ///     Gets or sets the locations.
        /// </summary>
        public List<FreeBaseSearchResult> Items { get; set; }

        /// <summary>
        ///     Gets or sets the order type.
        /// </summary>
        public string Keyword { get; set; }

        #endregion

        #region Methods

        /// <summary>
        ///   Return here your unselected items. First value is the ID you will store into your field, the second one is the display text.
        /// </summary>
        /// <returns> The unselected items </returns>
        protected override IEnumerable<KeyValuePair<string, string>> GetNonSelectedItems()
        {
            var selectedItems = new List<KeyValuePair<string, string>>();

            if (this.Items != null)
            {
                IEnumerable<KeyValuePair<string, string>> selected = this.GetSelectedItems();

                foreach (FreeBaseSearchResult item in this.Items)
                {
                    if (!string.IsNullOrEmpty(item.mid) && !string.IsNullOrEmpty(item.name))
                    {
                        var pair = new KeyValuePair<string, string>(item.mid, item.name);

                        KeyValuePair<string, string> exist =
                            selected.FirstOrDefault(
                                pItem => !string.IsNullOrEmpty(pItem.Key) && pItem.Key.Equals(item.mid));

                        if (string.IsNullOrEmpty(exist.Key))
                        {
                            selectedItems.Add(pair);
                        }
                    }
                }

                selectedItems = new List<KeyValuePair<string, string>>(selectedItems.OrderBy(pItem => pItem.Value));
            }

            return selectedItems;
        }

        /// <summary>
        ///   Return here your selected items. First value is the ID you will store into your field, the second one is the display text.
        /// </summary>
        /// <returns> The selected items </returns>
        protected override IEnumerable<KeyValuePair<string, string>> GetSelectedItems()
        {
            var selectedItems = new List<KeyValuePair<string, string>>();

            if (this.Items != null)
            {
                string[] fieldValues = this.Value.Split(new[] { '|' });

                if (fieldValues.Length > 0)
                {
                    foreach (string fieldValue in fieldValues)
                    {
                        if (!string.IsNullOrEmpty(fieldValue))
                        {
                            FreeBaseSearchResult itemsSelected =
                                this.Items.FirstOrDefault(
                                    pItem => !string.IsNullOrEmpty(pItem.mid) && pItem.mid.Equals(fieldValue));
                            if (itemsSelected != null)
                            {
                                var pair = new KeyValuePair<string, string>(itemsSelected.mid, itemsSelected.name);
                                selectedItems.Add(pair);
                            }
                        }
                    }
                }

                selectedItems = new List<KeyValuePair<string, string>>(selectedItems.OrderBy(pItem => pItem.Value));
            }

            return selectedItems;
        }

        /// <summary>
        ///     By overideing this method, you can initialise some variables here.
        /// </summary>
        protected override void ManageSource()
        {
            if (!string.IsNullOrEmpty(this.Source))
            {
                string[] pars = this.Source.Split(new[] { '|' });

                if (pars.Length > 0)
                {
                    foreach (string par in pars)
                    {
                        // Here you can add additional Source-Magic ;-)
                        if (!string.IsNullOrEmpty(par))
                        {
                            if (par.ToLower().StartsWith("keyword"))
                            {
                                string[] keyword = par.Split(new[] { '=' });

                                if (keyword.Length > 1)
                                {
                                    this.Keyword = keyword[1];
                                }
                            }
                        }
                    }
                }
            }

            if (this.Items == null && !string.IsNullOrEmpty(this.Keyword))
            {
                //REST call:
                using (var client = new WebClient())
                {
                    string responseXml = string.Empty;

                    try
                    {
                        responseXml =
                            client.DownloadString(
                                string.Format(
                                    "https://www.googleapis.com/freebase/v1/search?query={0}&indent=true", this.Keyword));
                    }
                    catch (Exception exc)
                    {
                        responseXml = "ERROR: " + exc.Message;
                    }

                    var search = JsonConvert.DeserializeObject<FreeBaseSearchItem>(responseXml);

                    if (search != null && search.result != null && search.result.Count > 0)
                    {
                        this.Items = search.result;
                    }
                }
            }
        }

        #endregion
    }
}

In der konkreten Klasse „FreeBaseMultilist“ habe ich eine zusätzliche Methode für die Verwaltung und Konfiguration des REST-Services implementiert. So kann bei der Template-Definition das Suchwort später durch einen Administrator/Author hinzugefügt werden. Damit wir den REST-Aufruf im Code typisiert weiterverwenden können, habe ich vorgängig entsprechende DTO’s (DataTypeObject’s) angelegt und über das Framework „Json.Net“ deserialisiert.

Folgend die DTO-Definition der FreeBase-Suche:

namespace Namics.Sample.CustomControls
{
    using System.Collections.Generic;
   
    public class FreeBaseSearchItem
    {
        public string status { get; set; }

        public List<FreeBaseSearchResult> result { get; set; }
    }

    public class FreeBaseSearchResult
    {
        public string mid { get; set; }

        public string id { get; set; }

        public string name { get; set; }

        public string lang { get; set; }

        public FreeBaseSearchNotable notable { get; set; }

        public string score { get; set; }
    }

    public class FreeBaseSearchNotable
    {
        public string name { get; set; }

        public string id { get; set; }
    }
}

Registrierung am System
Damit unser Control im Sitecore erkannt wird, muss unsere Assembly in der web.Config definiert werden (unterster Eintrag):

<controlSources>
      <source mode="on" namespace="Sitecore.Web.UI.XmlControls" folder="/sitecore/shell/override" deep="true" />
      <source mode="on" namespace="Sitecore.Web.UI.XmlControls" folder="/layouts" deep="false" />
      <source mode="on" namespace="Sitecore.Web.UI.XmlControls" folder="/sitecore/shell/controls" deep="true" />
      <source mode="on" namespace="Sitecore.Web.UI.XmlControls" folder="/sitecore/shell/applications" deep="true" />
      <source mode="on" namespace="Sitecore.Web.UI.XmlControls" folder="/sitecore modules" deep="true" />
      <source mode="on" namespace="Sitecore.Web.UI.HtmlControls" assembly="Sitecore.Kernel" />
      <source mode="on" namespace="Sitecore.Web.UI.WebControls" assembly="Sitecore.Kernel" />
      <source mode="on" namespace="Sitecore.Shell.Web.UI.WebControls" assembly="Sitecore.Kernel" prefix="shell" />
      <source mode="on" namespace="Sitecore.Shell.Applications.ContentEditor" assembly="Sitecore.Kernel" prefix="content" />
      <source mode="on" namespace="Sitecore.Shell.Web.Applications.ContentEditor" assembly="Sitecore.Kernel" prefix="shell" />
      <source mode="on" namespace="Sitecore.WebControls" assembly="Sitecore.Kernel" />
      <source mode="on" namespace="System.Web.UI.WebControls" assembly="System.Web" prefix="asp" />
      <source mode="on" namespace="System.Web.UI.HtmlControls" assembly="System.Web" prefix="html" />
      <source mode="on" namespace="Sitecore.Web.UI.Portal" assembly="Sitecore.Kernel" />
      <source mode="on" namespace="ComponentArt.Web.UI" assembly="ComponentArt.Web.UI" prefix="ca" />
      <source mode="on" namespace="Namics.Sample.CustomControls" assembly="Namics.Sample.CustomControls" prefix="namics" />
    </controlSources>

Dieses Control wird nun in der Laufzeitumgebung von Sitecore registriert. Damit dieses Control auch durch einen Template-Administrator selektiert werden kann, muss dies in der Sitecore Core-DB unter /sitecore/system/Field types/List Types/ als Item angelegt werden:

blog_sitecore_freebaseField_core-definition

Let’s play
Nach erfolgreicher Konfiguration in der Core-Datenbank steht unser FreeBase-Control nun für das Template-Building zur Verfügung. Durch unsere Modifikation der Field-Source können wir nun mittels keyword=meinSchlüsselwort direkt eine spezifische Suche ausgeführt werden:

blog_sitecore_freebaseField_template

Bei der Initialisierung eines Items, kann nun das FreeBase-Feld mit der Source-Definition verwendet und administriert werden:

blog_sitecore_freebaseField

Im Code unterscheiden wir zwischen Anzeigetext und Wert. Der Anzeigetext sollte dem Author ermöglichen eine richtige Konfiguration durch lesbare/verständliche Textinhalte auszuführen – im Hintergrund benötigt das System jedoch eindeutige Werte für die Weiterverarbeitung der Authorenselektion. In diesem Beispiel gebe ich als Anzeigetext das JSON-Property „name“ an und als eindeutige Identifikation das Property „mid“. Ersichtlich wird das alles in den Raw-Values:

blog_sitecore_freebaseField_raw

Mit diesen Codezeilen kann somit ein Control auf einen Fremddienst zugreifen, der Author hat die Möglichkeit damit Einstellungen vorzunehmen und das System und die Renderings können mit den gegebenen eindeutigen Werten Funktionen und Präsentationen ausführen. Zum Beispiel ein simpler Hyperlink auf die „mid“: http://www.freebase.com/m/046387d

…Letztendlich eine runde Sache 😉

4 Gedanken zu “MultilistField in Kommunikation mit WebServices

  1. Pingback: Dynamische Source Eigenschaft eines Feldes – Sitecore

  2. Guter Punkt! Ja, die API war bis 31.08.2016 noch aktiv und der Blog-Artikel ist aus dem Jahre 2013. Bedeutet also, dass ich das Thema mal wieder mit einem anderen Control mit einem anderen Service wiederholen kann 😉

    Danke für den Hinweis!

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

*

*

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>