NameLookupList field with query and dynamic item/field sources

Sitecore provides NameValueList & NameLookupValueList fields for storing Key/Value pair of dynamic n number of data. These fields are highly usefull though they don’t come to use that often like Treelist or Multilist fields.

Recently, for one of my requirements, i was needed to lookup the key instead of the values. And values will be entered manually. I was already aware of the such custom field created by @jammykam which you can find here: https://jammykam.wordpress.com/2015/03/08/custom-sitecore-field-for-storing-keyvalue-data-with-lookups/ 

As the field was already there, i just needed to utilize that in my solution. But there was one additional requirement to have datasource of this field using query to find out ancestor and use it as a datasource. Above field does not support the query as source. I found below list of fields and which fields support query and special syntax: https://stackoverflow.com/questions/12611258/query-notation-for-the-sitecore-source-field-in-template-builder/

Fields that support Sitecore Query

Where you can use the syntax query: or fast: (for FastQuery):

  • Droplist
  • Grouped Droplist
  • DropLink
  • Grouped Droplink
  • Checklist
  • Multilist
  • Droptree

Fields that support Parameterized Datasource

Where you can use enhanced syntax with Parameterized datasource query:

  • Droptree – only supports the Datasource and DatabaseName parameters.
  • Treelist
  • TreelistEx

The enhanced query string syntax includes the following parameters:

  • Datasource
  • DatabaseName
  • AllowMultipleSelection
  • IncludeItemsForDisplay
  • ExcludeItemsForDisplay
  • IncludeTemplatesForSelection
  • ExcludeTemplatesForSelection
  • IncludeTemplatesForDisplay
  • ExcludeTemplatesForDisplay

You can find out more about parameterized datasource: http://firebreaksice.com/tame-your-sitecore-treelists/

Any other ways?

Yes. Using code: in your source field. Read more: https://horizontalintegration.blog/2015/04/02/sitecore-source-field-code-query-that-implements-idatasource/

This seems powerful option to have datasource based on complex business logic or external data as a source.

What i wanted to achieve?

  1. NameLookupList field – This is already available in community
  2. Query as source field in NameLookupList – Already have examples of TreeList control implementing this which i need to incorporate into this.
  3. Consider selected values of any List field from item as a source to this field – https://ctor.io/dynamic-field-sources-with-getlookupsourceitems-pipeline/ this link describes how to do that by adding custom processor to getLookupSourceItems pipeline. But it takes the field from context item. Which i want to extend with dynamic source item.

Everything Together

NameLookupList field which supports Query

using System;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.UI;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Text;
using Sitecore.Web.UI.HtmlControls.Data;
using Sitecore.Web.UI.Sheer;

namespace sc9.Fields
{
  public class NameLookupList : NameLookupValue
  {
    public new string Source
    {
      get => base.Source;
      set
      {
        if (value == null)
        {
          base.Source = null;
        }
        else
        {
          var datasource = StringUtil.ExtractParameter("DataSource", value).Trim();
          if (datasource.StartsWith("query:"))
          {
            value = value.Replace(datasource, ResolveQueryBasedDatasource(datasource));
          }
          base.Source = value;
        }
      }
    }

    private string ResolveQueryBasedDatasource(string query)
    {
      if (Sitecore.Context.ContentDatabase == null || ItemID == null)
      {
        return null;
      }
      var current = Sitecore.Context.ContentDatabase.GetItem(ItemID);
      Item item = null;
      try
      {
        item = LookupSources.GetItems(current, query).FirstOrDefault();
      }
      catch (Exception ex)
      {
        Log.Error("NameLookupList field failed to execute query.", ex, this);
      }
      return item?.Paths.FullPath;
    }

    protected override void OnLoad(EventArgs e)
    {
      Assert.ArgumentNotNull((object)e, "e");

      string origValue = this.Value;
      base.OnLoad(e); // this will modify this.Value using existing base logic and falsely throw a modified alert
      this.Value = origValue; // so reset the value back for comparison below
      Sitecore.Context.ClientPage.Modified = false; // if values have actually changed our code below will set this to true

      UrlString urlString = new UrlString(this.Value);
      if (Sitecore.Context.ClientPage.IsEvent)
        this.LoadValue();
      else
        this.BuildControl();
    }

    private void LoadValue()
    {
      if (this.ReadOnly || this.Disabled)
        return;
      System.Web.UI.Page page = HttpContext.Current.Handler as System.Web.UI.Page;
      NameValueCollection nameValueCollection = page == null ? new NameValueCollection() : page.Request.Form;
      UrlString urlString = new UrlString();
      foreach (string index1 in nameValueCollection.Keys)
      {
        if (!string.IsNullOrEmpty(index1) && index1.StartsWith(this.ID + "_Param", StringComparison.InvariantCulture) && !index1.EndsWith("_value", StringComparison.InvariantCulture))
        {
          string input = nameValueCollection[index1];
          string str = nameValueCollection[index1 + "_value"];
          if (!string.IsNullOrEmpty(input))
          {
            // modified here to return the actual GUID
            urlString[input] = str;
          }
        }
      }
      string str1 = urlString.ToString();
      if (this.Value == str1)
        return;
      this.Value = str1;
      this.SetModified();
    }

    private void BuildControl()
    {
      this.Controls.Clear(); /* clear controls created by base class */
      UrlString urlString = new UrlString(this.Value);
      foreach (string key in urlString.Parameters.Keys)
      {
        if (key.Length > 0)
          this.Controls.Add((System.Web.UI.Control)new LiteralControl(this.BuildParameterKeyValue(key, urlString.Parameters[key])));
      }
      this.Controls.Add((System.Web.UI.Control)new LiteralControl(this.BuildParameterKeyValue(string.Empty, string.Empty)));
    }

    protected new void ParameterChange()
    {
      ClientPage clientPage = Sitecore.Context.ClientPage;
      if (clientPage.ClientRequest.Source == StringUtil.GetString(clientPage.ServerProperties[this.ID + "_LastParameterID"]) && !string.IsNullOrEmpty(clientPage.ClientRequest.Form[clientPage.ClientRequest.Source]))
      {
        string str = this.BuildParameterKeyValue(string.Empty, string.Empty);
        clientPage.ClientResponse.Insert(this.ID, "beforeEnd", str);
      }
      NameValueCollection form = (NameValueCollection)null;
      System.Web.UI.Page page = HttpContext.Current.Handler as System.Web.UI.Page;
      if (page != null)
        form = page.Request.Form;
      if (form == null) // || !this.Validate(form)) -- removed validation of Key field
        return;
      clientPage.ClientResponse.SetReturnValue(true);
    }

    private string BuildParameterKeyValue(string key, string value)
    {
      Assert.ArgumentNotNull((object)key, "key");
      Assert.ArgumentNotNull((object)value, "value");
      string uniqueId = GetUniqueID(this.ID + "_Param");
      Sitecore.Context.ClientPage.ServerProperties[this.ID + "_LastParameterID"] = (object)uniqueId;
      string clientEvent = Sitecore.Context.ClientPage.GetClientEvent(this.ID + ".ParameterChange");
      string str1 = this.ReadOnly ? " readonly=\"readonly\"" : string.Empty;
      string str2 = this.Disabled ? " disabled=\"disabled\"" : string.Empty;
      string str3 = this.IsVertical ? "</tr>
<tr>" : string.Empty;
      return
          string.Format(
              "
<table width="100%" cellpadding="4" cellspacing="0" border="0">
<tr>
<td>{0}</td>
{2}
<td width="100%">{1}</td>
</tr>
</table>
",
              GetNameLookupHtmlControl(uniqueId, str1, str2, key, clientEvent), /* This code has been changed from default */
              (object)this.GetValueHtmlControl(uniqueId, StringUtil.EscapeQuote(HttpUtility.UrlDecode(value))),
              (object)str3);
    }

    private string GetNameLookupHtmlControl(string uniqueId, string readOnly, string disabled, string key, string clientEvent)
    {
      HtmlTextWriter htmlTextWriter = new HtmlTextWriter((TextWriter)new StringWriter());
      Item[] items = this.GetItems(Sitecore.Context.ContentDatabase.GetItem(this.ItemID));

      htmlTextWriter.Write("", uniqueId, this.NameStyle, readOnly, disabled, clientEvent);
      htmlTextWriter.Write("
");
      foreach (Item obj in items)
      {
        string itemHeader = this.GetItemHeader(obj);
        bool flag = obj.ID.ToString() == key;
        htmlTextWriter.Write("
" + itemHeader + "");
      }
      htmlTextWriter.Write("
");
      return htmlTextWriter.InnerWriter.ToString();
    }

    // Copied back from the original NameValue control that NameLookupValue overrode
    protected override string GetValueHtmlControl(string id, string value)
    {
      string str1 = this.ReadOnly ? " readonly=\"readonly\"" : string.Empty;
      string str2 = this.Disabled ? " disabled=\"disabled\"" : string.Empty;
      return string.Format("", (object)id, (object)value, (object)str1, (object)str2);
    }
  }
}

You will also required to create the custom field item in core database and below configuration:








GetLookupSourceFromField processor

using Sitecore.Pipelines.GetLookupSourceItems;
using System;
using System.Collections.Generic;
<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>using System.Linq;
using System.Web;

namespace sc9.Fields
{
  public class GetLookupSourceFromField
  {
    private const string FromFieldParam = "field";
    private const string DataSourceFieldParam = "DataSource";

    public void Process(GetLookupSourceItemsArgs args)
    {
      // check if "field" is available in the source
      if (!args.Source.Contains(FromFieldParam))
      {
        return;
      }

      // get the field and DataSource
      var parameters = Sitecore.Web.WebUtil.ParseUrlParameters(args.Source);
      var fieldName = parameters[FromFieldParam];
      var dataSource = parameters[DataSourceFieldParam];

      if (Sitecore.Context.ContentDatabase == null)
        return;

      var sourceItem = Sitecore.Context.ContentDatabase.GetItem(dataSource);

      if (sourceItem == null)
        sourceItem = args.Item;

      if (string.IsNullOrEmpty(sourceItem[fieldName]))
        return;

      // set the source to a query with all items from the other field included
      var items = sourceItem[fieldName].Split('|');
      args.Source = this.GetDataSource(items);
    }

    private string GetDataSource(IList items)
    {
      if (!items.Any()) return string.Empty;

      var query = items.Aggregate(string.Empty, (current, itemId) =&gt; current + string.Format(" or @@id='{0}'", itemId));
      return string.Format("query://*[{0}]", query.Substring(" or ".Length));
    }
  }
}








</pre>

How to use it in source?

  1. You can provide the path or id of the item as we provide normally: /sitecore/content/Global Repository/Products
  2. You can use static datasource as above and include field whose selected values you want as dataource: DataSource=/sitecore/content/Global Repository/Product1&field=Categories
  3. You can use simply query to define the datasource just like the other fields: query:./ancestor-or-self::*[@@templateid='{5728B1B6-DA02-4D62-BF86-BB962B408E8E}’]/*[@@templateid='{C113D826-C6B9-4DC7-9B7B-89D0C6272144}’]//*
  4. You can also use query and include field whose selected values you want as datasource: DataSource=query:./ancestor-or-self::*[@@templateid='{28103498-4995-4000-A0DB-1D3C2A757C79}’]&field=Categories

You can find the above source code here.

Utilize the power of sitecore while defining source/datasource of the field to give your content editor a great experience.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s