Multlistfield Search

In this post I would like to discuss the features of a fairly unknown field type: the Multilist with Search.

This field type was introduced in Sitecore 7 to be able to have Multilist fields offering many items for selection, potentially even using an item bucket as the source for those items.

The traditional Multilist field is very useful, but as the number of selections increases the lack of any search functionality weighs heavily in its usability. The new field type adds a search box and paging to help manage large amounts of items. It also includes a novel way of defining the source. As it is querying the index directly it offers more functionality that could be captured with just an ID or a Sitecore query (the typical sources of a standard Multilist).

Unfortunately this new field type comes loaded with its own idiosyncrasies. So much so that it has taken me days of research to complete this seemingly simple blog post.

The Basics

As explained above, the Multilist with Search field is essentially a multilist field with a search box and paging:

multilistwithsearch

Its field source is encoded like a query string (key=value&key2=value2). To avoid showing the entire tree, it requires at the very least, the parent item of the items to be shown. Remember though we are not querying the database but the search index. This is done with the StartSearchLocation attribute. It can point to an ID, or just like with Multilists, it can also be a query StartSearchLocation=query:.. This is where the fun starts.

Should we want to do a more complicated query like query:ancestor-or-self::*[@@templatekey='folder'] the equal sign inside the constraint is misunderstood due to the (very naive) parsing of the field source. They provide a simple solution, replace equals inside the query for '->' hence the field source should be written as: StartSearchLocation=query:ancestor-or-self::*[@@templatekey->'folder']. Except this produces an error

error screen due to query

It turns out that at some point in the code they Javascript encode the whole field source but they forget to decode it before sending it to the query parser.

The solution is to override the field type. Create a class

using Sitecore.Buckets.FieldTypes;  
using System.Web;  
namespace Sitecore.Custom.Fields  
{
     public class CustomMultilistSearch: BucketList
     {
          protected override string MakeFilterQueryable(string locationFilter)
          {
               var newFilter = HttpUtility.UrlDecode(locationFilter.Replace("\\u00", "%"));
               return base.MakeFilterQueryable(newFilter);
          }
     }
}

Go to the field definition item in the Core database under the location /sitecore/system/Field types/List Types/Multilist with Search, delete the Control field and fill the Assembly and Class so it points to the class above.

More trouble ahead

With the fix above in place we can use any query we want.

field with values

We can change pages to get more results, and return to the first page

return to the first page

Has anything changed? Notice that it displays things differently AND we have some items repeated!

The repeated items are actually multiple versions of the same item. When the field is first rendered, it goes through the standard contentSearch.getGlobalSearchFilters pipeline (as expected with all searches invoked from the UI) and it gets an extra query filter on the _latestversion computed field to ensure it only returns one result regardless of the number of versions. Of course the downside is that you are only searching for content in the latest version of each item, I am sure most users are unaware of this1.

When first rendering the field it goes through that pipeline, but when changing pages or performing a search it does not. That's why you get a different set of results.

Of course in the case of a multilist with search you probably don't want to get one result per version. At the end of the day you are selecting items, IDs, so the version is irrelevant.

I looked for a solution to this using pipelines, overriding code, and all kinds of hackery, I failed.

The fix

But it turns out there is a simple fix after all. You just need to leverage another feature of this field source. Let's declare our source as StartSearchLocation=query:..&Filter=_latestversion:true The filter attribute allows us to pass a query to the index. We can even define multiple fields by using a pipe separator, and in theory + and - for 'must' and 'must not' operators. (There are issues with the plus operator, more info below).

So now even though the pipeline is not run when changing pages we are forcing the attribute ourselves as part of the source of the field.

We still have the small issue about the formatting of the names. Initially they carry the language and version, when refreshing the page, only the display name, template and name of parent. We can tackle this in two ways, override the Javascript code that renders the results after a search, or modify the original field type that renders the field initially. Since we are already overriding the field to fix Sitecore query support, let's follow the latter approach.
Let's add another method to the class created previously:

public override string OutputString(Item item)  
{
     Item bucketItemOrParent = ItemExtensions.GetParentBucketItemOrParent(item);
     string str = bucketItemOrParent != null ? "- " + bucketItemOrParent.DisplayName : string.Empty;
     return string.Format("{0} ({1} {2})", item.DisplayName, item.TemplateName, str);
}

This code also corrects the little issue where the name of the parent folder (or bucket) is using the item name instead of the more correct display name.

Languages

We have other attributes we can play with. We have Language which limits the results to a particular language. Unfortunately this has changed recently. Whereas initially you would define the language with its code (e.g. "en") now you need the full name ("English"). This is a little bit annoying, so I suggest you use the Filter attribute instead: StartSearchLocation=query:..&Filter=+_latestversion:true|+_language:en.

Notice the 'plus' signs. This is to force items to be the latest version and in English. If we omit the plus signs then results that meet only one of the two criteria would also show up.

But we have another problem. This works for the first rendering of the field, but again, things go awry when you change page or perform a search. It so happens that they do not encode the plus signs, and they get understood as spaces in the URL, which in turn are replaced by underscores when transforming them into field names. The solution? Well you can attack the problem in the Javascript code, encoding them before submitting the Ajax, or in the field code. Again, since we are already overriding the field, let's do it there.

 protected override string ScriptParameters
 {
    get
    {
       var filter = Filter.Replace("+","%2B");
       return string.Format("'{0}'", string.Join("', '", this.ID, this.ClientID, this.PageNumber, (object)"/sitecore/shell/Applications/Buckets/Services/Search.ashx", filter, SearchHelper.GetDatabaseUrlParameter("&"), this.TypeHereToSearch, this.Of, (this.EnableSetNewStartLocation ? 1 : 0)));
    }
}

It might well be that you don't want to force a language, instead you want to use the current context language. We could accommodate this requirement too:

protected override Sitecore.Data.Items.Item[] GetItems(Sitecore.Data.Items.Item current)  
{
    this.Source = this.Source.Replace("$currentlanguage", ItemLanguage);
    return base.GetItems(current);
}

Now we could write: StartSearchLocation=query:..&Filter=+_latestversion:true|+_language:$currentlanguage

Other parameters

We have a few other options available:

  • FullTextQuery=searchterm attribute, which would be exactly the same as doing Filter:_content:searchterm. It will only show items that match that search.
  • SortField which obviously specifies the field for sorting. Beware though of using the name or display name of the item. Those fields are tokenized so you might not get the expected result if your item name has spaces.
    Blue note sorted incorrectly
    You are better off using the fullpath field instead (which is not tokenized).

  • TemplateFilter which does not work. The code tries to look for the 'template' field whereas it should look for _template (Guid) or _templatename . This is not a big problem, as you can just append those restrictions to the Filter attribute.

  • EnableSetNewStartLocation=true will allow the user to override the StartSearchLocation attribute by typing (unfortunately) an ID. start search location field

Overview

This field type has quite a few issues but they can all be fixed quite easily. You can use the following cheat sheet

AttributeNotes
StartSearchLocationAn ID or a query. Replace = for ->. Needs fix to un-encode characters. Don't use fast: it's broken
FilterPipe separated fields. At the very least add _latestversion:true to stop multiple versions from coming as different results. Needs a fix to support + operator to 'and' multiple criteria
LanguageRequires the full language name. Otherwise use _language:en in a Filter
FullTextQuerySame as looking in _content field
SortFieldCareful when sorting with _name or _displayname and having spaces. Use fullpath instead
PageSizeDefault is 10
TemplateFilterDoes not work. Use _templatename in a Filter
EnableSetNewStartLocationTakes a boolean, defaults to false. Will allow users to add ID of new start search location

  1. Do the test, create an item with 2 versions, put some string in one of the fields in version 1, and don't put it in version 2. Look for that string using the UI. Nothing will appear.