Tuesday, March 22, 2016

Custom index update strategy

ComputedIndexField with dependencies to other items

Ever had a ComputedIndexField that gathered data from other items than the item currently being indexed? For example, from its children. Or from items referred to..  I just had a situation where we needed (a property of) the children to be included in our ComputedIndexField.
But what happens if you update a child item? The child is re-indexed but the parent is not as this was not changed, not published, ...  We were using the onPublishEndAsync update strategy and didn't want to have a solution that needed a rebuild periodically just to keep the index up to date.

There is a GetDependencies pipeline that can be used to add extra items to the indexable list, but that was no option as this pipeline is for all indexes and we wanted it just for our custom index and preferably configurable per index (thinking about performance as well..)

Extend the onPublishEnd update strategy

So, we started thinking of extending the update strategy. We found some examples on the internet but not in our Sitecore version (we are using Sitecore 8.1) and the examples didn't go far enough.

What we wanted was:
  • the base onPublishEnd strategy (that would still check for rebuilds and so on)
  • an extension that would add the first ascendant of the item of a defined template to the list of indexable items

I had a look at the code of the OnPublishEndAsynchronousStrategy with DotPeek and noticed that this was extendable indeed. 

Let's start by creating our class by doing what a developer is good at: copy/paste :)

[DataContract]
public class OnPublishEndWithAncestorAsynchronousStrategy : OnPublishEndAsynchronousStrategy
{
    public string ParentTemplateId { get; set; }
    public string ChildTemplateId { get; set; }

    public OnPublishEndWithAncestorAsynchronousStrategy(string database) : base(database)
    {
    }
}

We created a class that extends OnPublishEndAsynchronousStrategy and gave it a constructor that needs the database name (which will be passed in the config). We also defined two variables to identify the templates that are affected - both parent (ancestor item to look for) as child (item to start from).

Performance

The child item template(s) are requested because our strategy code is executed before the crawler's root path is checked and before the 'DocumentOptions' are checked (like 'IncludeTemplates'). As this extended strategy is already heavier than the original one we wanted to prevent getting even more performance hits for items we don't need to check. This will become clear later on...

Configuration


<strategies hint="list:AddStrategy">
  <onPublishEndWithAncestorAsync type="Xx.OnPublishEndWithAncestorAsynchronousStrategy, Xx">
    <param desc="database">web</param>
    <ParentTemplateId>{0F5141D6-F264-4D03-B5D2-3505E6F308E7}</ParentTemplateId>
    <ChildTemplateId>{2A993FF2-5F17-4EEA-AD53-5343794F86BB}{066DEA00-31D7-4838-94A6-8D05A7FC690E}</ChildTemplateId>
  </onPublishEndWithAncestorAsync>
</strategies>

In the strategies section where you normally add your strategies by pointing towards the one(s) defined in the default Sitecore index configurations we define our custom strategy by providing the type. We send the database (web) as parameter and define the guids for the templates. In this example code we can send multiple child templates.

The index run

After some investigation, it turned out we only had to override one method: "Run". 
We started by copy/pasting the original code and checked the extension points:
  • if the item queue is empty: we leave the original code 
  • if the item queue is so big a rebuild is suggested: we keep the original code as a rebuild will also update the ancestor we might add
  • else..
We kept the original code to fetch the list of items to refresh. We don't actually get the items as "Item" but as "IndexableInfo" objects. For each entry in this list we call our GetAncestor function. The result is checked for null and added to the original list only if is wasn't already in there.


protected override void Run(List<QueuedEvent> queue, ISearchIndex index)
{
    CrawlingLog.Log.Debug($"[Index={index.Name}] {GetType().Name} executing.");
    if (Database == null)
    {
        CrawlingLog.Log.Fatal($"[Index={index.Name}] OperationMonitor has invalid parameters. Index Update cancelled.");
    }
    else
    {
        queue = queue.Where(q => q.Timestamp > (index.Summary.LastUpdatedTimestamp ?? 0L)).ToList();
        if (queue.Count <= 0)
        {
            CrawlingLog.Log.Debug($"[Index={index.Name}] Event Queue is empty. Incremental update returns");
        }
        else if (CheckForThreshold && queue.Count > ContentSearchSettings.FullRebuildItemCountThreshold())
        {
            CrawlingLog.Log.Warn($"[Index={index.Name}] The number of changes exceeded maximum threshold of '{ContentSearchSettings.FullRebuildItemCountThreshold()}'.");
            if (RaiseRemoteEvents)
            {
                IndexCustodian.FullRebuild(index).Wait();
            }
            else
            {
                IndexCustodian.FullRebuildRemote(index).Wait();
            }
        }
        else
        {
            var list = ExtractIndexableInfoFromQueue(queue).ToList();
            // custom code start here...
            CrawlingLog.Log.Info($"[Index={index.Name}] Found '{list.Count}' items from Event Queue.");
            var result = new List<IndexableInfo>();
            CrawlingLog.Log.Info($"[Index={index.Name}] OnPublishEndWithAncestorAsynchronousStrategy executing.");
            foreach (var itemInfo in list)
            {
                var ancestor = GetAncestor(itemInfo);
                if (ancestor != null)
                {
                    if (list.Any(i => i.IndexableId.Equals(ancestor.IndexableId, StringComparison.OrdinalIgnoreCase)))
                    {
                        CrawlingLog.Log.Info($"[Index={index.Name}] Ancestor already in list '{ancestor.IndexableId}'.");
                    }
                    else
                    {
                        CrawlingLog.Log.Info($"[Index={index.Name}] Adding ancestor '{ancestor.IndexableId}'.");
                        result.Add(ancestor);
                    }
                }
            }

            list.AddRange(result);
            CrawlingLog.Log.Info($"[Index={index.Name}] Updating '{list.Count}' items.");
            IndexCustodian.IncrementalUpdate(index, list).Wait();
        }
    }
}

Job(s)

One of the noticeable things here is that we add the extra indexable items to the existing list called with the incremental update. We could also call the Refresh method on the IndexCustodian but that would create extra (background) jobs so this way seems more efficient.

The ancestor check

Last thing to do is the ancestor check itself. For our requirements we needed to find an ancestor of a defined template but this functions could actually do anything. Just keep in mind the performance as this function will be called a lot.. (any ideas how to further improve this are welcome)

private IndexableInfo GetAncestor(IndexableInfo info)
{
    try
    {
 var childTemplateId = ChildTemplateId.ToLowerInvariant();
 var item = Database.GetItem(((ItemUri)info.IndexableUniqueId.Value).ItemID);
 if (item != null && childTemplateId.Contains(item.TemplateID.Guid.ToString("B")))
 {
     var ancestor = item.Axes.GetAncestors().ToList().FindLast(i => i.TemplateID.Guid.ToString("B").Equals(ParentTemplateId, StringComparison.OrdinalIgnoreCase));
     if (ancestor != null)
     {
  return new IndexableInfo(
                        new SitecoreItemUniqueId(
                            new ItemUri(ancestor.ID, ancestor.Language, ancestor.Version, Database)), 
                            info.Timestamp)
    {
        IsSharedFieldChanged = info.IsSharedFieldChanged
    };
     }
 }
    }
    catch (Exception e)
    {
 CrawlingLog.Log.Error($"[Index] Error getting ancestor for '{info.IndexableId}'.", e);
    }

    return null;
}


Using the child template in the config as well, might seems like a limitation but here it gives us a good performance gain because we limit the number of (slow) ancestor look-ups a lot. We still need to do that first lookup of the actual item to detect the template though.
We catch all exceptions - ok, might be bad practice - just to make sure in our test that one failure doesn't break it all.

Conclusion

As usual, we managed to tweak Sitecore in a fairly easy manor. This example can hopefully lead you towards more optimizations and other implementations of custom index features. Suggestions and/or improvements are welcome...


Tuesday, March 1, 2016

Query.MaxItems in Sitecore 8.1

Small tip for people using Query.MaxItems and upgrading to 8.1


Setting the maximum number of results from a Query

As you might know Sitecore has a setting called "Query.MaxItems". The default value of this setting is 100 (well, in the base config file). Setting this value to 0 will make queries return all results without limit - which might (will) have a negative impact on performance in case of a large result set.

We aware that this setting not only influences your own queries, but also some api calls (Axes, SelectItems, ...) and Sitecore fields that use queries underneath. It does not affect fast queries.


The Query.MaxItems value in Sitecore 8.1 : 260

Using queries is in lots of cases not a good idea and as a fan of indexes I (almost) never use them myself but some questions came up when people were upgrading so I decided to blog this little tip: 
in Sitecore 8.1 the Query.MaxItems value is patched in Sitecore.ExperienceExplorer.config and set to 260
If you patched the value yourself and did not use a separate include file (as you should!) and did not take care that your include file comes at the end (prefix with z is a common trick - using a subfolder starting with z- is a better one) this new value of 260 will overwrite yours.

Friday, February 26, 2016

Custom indexes in a Sitecore Helix architecture

Sitecore Helix/Habitat

Most Sitecore developers probably know what Sitecore Habitat and Sitecore Helix is about right now, especially since the 2016 Hackathon. Several interesting blog posts have already been written about the architecture (e.g. this one by Anders Laub) and video's have been posted on Youtube by Thomas Eldblom.

Custom indexes

I use custom indexes very frequently. So I started thinking about how I could use custom indexes in a "Helix" architecture. When creating a feature that uses such a custom index, the configuration for that index has to be in the feature. That is perfectly possible as we can create a separate index config file. But what are the things we define in that config file?

  • index
    • a name
    • the strategy
    • crawler(s)
    • ...
  • index configuration
    • field map
    • document options
      • computed index fields
      • include templates
      • ...
    • ...

Some of these settings can be defined in our feature without issue: the name -obviously- and the stategy (e.g. onPublishEndAsync) can be defined. A crawler might be the first difficulty but this can be set to a very high level (e.g. <Root>/sitecore/content</Root>)

In the index configuration we can also define the fieldMap and such. In the documentOptions section we can (must) define our computed index fields. But then we should define our included templates. And that was were I got stuck..  in a feature I don't know my template, just base templates..

Patching

A first thought was to use the patching mechanism from Sitecore. We could define our index and it's configuration in the feature and patch the included templates and/or crawlers in the project.
Sounds like a plan, but especially for the included templates it didn't feel quite right.

For the index itself patching will be necessary in some cases.. e.g. to enable or disable Item/Field language fallback. If needed it is also possible to patch the content root in the project level.

Included templates? Included base templates!

For the included templates in the document options I was searching for another solution so I decided to throw my question on the Sitecore Slack Helix/Habitat channel and ended up in a discussion with Thomas Eldblom and Sitecore junkie Mike Reynolds. Thomas came up with the idea to hook into the index process to enable it to include base templates and Mike kept pushing me to do it and so.. I wrote an extension to configure your index based on base templates.

The code is a proof of concept.. it can -probably- be made better still but let this be a start.


Custom document options

I started by taking a look at one of my custom indexes to see what Sitecore was doing with the documentOptions sections and took a look at their code in Sitecore.ContentSearch.LuceneProvider.LuceneDocumentBuilderOptions. As you can guess, the poc is done with Lucene..

Configuration

The idea was to create a custom document options class by inheriting from the LuceneDocumentBuilderOptions. I could add a new method to allow adding templates in a new section with included base templates. This will not break any other configuration sections.

An example config looks like:
<documentOptions type="YourNamespace.TestOptions, YourAssembly">
    <indexAllFields>true</indexAllFields>
    <include hint="list:AddIncludedBaseTemplate">
        <BaseTemplate1>{B6FADEA4-61EE-435F-A9EF-B6C9C3B9CB2E}</BaseTemplate1>
    </include>
</documentOptions>
This looks very familiar - as intended. We create a new include section with the hint "list:AddIncludedBaseTemplate". The name 'AddIncludedBaseTemplate' will come back later in our code.

Code

Related templates

The first function we created was to get all templates that relate to our base template:

private IEnumerable<Item> GetLinkedTemplates(Item item)
{
  var links = Globals.LinkDatabase.GetReferrers(item, new ID("{12C33F3F-86C5-43A5-AEB4-5598CEC45116}"));
  if (links == null)
  {
    return new List<Item>();
  }

  var items = links.Select(i => i.GetSourceItem()).Where(i => i != null).ToList();
  var result = new List<Item>();
  foreach (var linkItem in items)
  {
    result.AddRange(GetLinkedTemplates(linkItem));
  }

  items.AddRange(result);
  return items;
}

We use the link database to get the referrers and use the Guid of the "Base template" field of a template to make sure that we get references in that field only - which also makes sure that all results are actual Template items.
The function is recursive because a template using your base template can again be a base template for another template (which will by design also include your original base template). The result is a list of items.

A second function will use our first one to generate a list of Guids from the ID of the original base template:
public IEnumerable<string> GetLinkedTemplates(ID id)
{
  var item = Factory.GetDatabase("web").GetItem(id);
  Assert.IsNotNull(item, "Configuration : templateId cannot be found");

  var linkedItems = GetLinkedTemplates(item);
  return linkedItems.Select(l => l.ID.Guid.ToString("B").ToUpperInvariant()).Distinct();
}

As you can see what we do here is try to fetch the item from the id and call our GetLinkedTemplates function. From the results we take the distinct list of guid-strings - in uppercase.

Context database

One big remark here is the fact that I don't know what the database is - if somebody knows how to do that, please let me (and everybody) know. The context database in my tests was 'core' - I tried to find the database defined in the crawler because that is the one you would need but no luck so far.

And finally...

AddIncludedBaseTemplate

public virtual void AddIncludedBaseTemplate(string templateId)
{
  Assert.ArgumentNotNull(templateId, "templateId");
  ID id;
  Assert.IsTrue(ID.TryParse(templateId, out id), "Configuration: AddIncludedBaseTemplate entry is not a valid GUID. Template ID Value: " + templateId);
  foreach (var linkedId in GetLinkedTemplates(id))
  {
    AddTemplateFilter(linkedId, true);
  }
}

Our main function is called "AddIncludedBaseTemplate" - this is consistent with the name used in the configuration. In the end we want to use the "AddTemplateFilter" function from the base DocumentBuilderOptions - the 'true' parameter is telling the function that the templates are included (false is excluded). So we convert the template guid coming in to an ID to validate it and use it in the functions we created to get all related templates.

Performance

Determining your included templates is apparently only done once at startup. So if you have a lot of base templates to include which have lots of templates using them, don't worry about this code being called on every index update. Which of course doesn't mean we shouldn't think about performance here ;)

Conclusion

So we are now able to configure our index to only use templates that inherit from our base templates. Cool. Does it end here? No.. you can re-use this logic to create other document options as well to tweak your index behavior. 

And once more: thanks to Thomas & Mike for the good chat that lead to this.. The community works :)

Wednesday, February 24, 2016

Translating WFFM 8.1 MVC forms error messages part II

Multilingual WFFM

The first post in this duo-series of posts handled the client-side validation of Sitecore's WebForms for Marketers. In this post I want to focus on the server-side error messages: changing them, translating them and the pitfalls you might encounter doing so.

Server-side validation and error messages

Server side validation is a must in forms - no need to tell you that. When using MVC forms you can alter the default messages here:
/sitecore/system/Modules/Web Forms for Marketers/Settings/Meta data/Mvc Validation Error Messages



Translating

As the "value" field of these items is not shared, one would assume that translating the error messages is nothing more than creating versions in other languages and filling in your translated message.
And yes, at first sight you might think that works as you will get to see error messages in a language that could be something else then English and could be even the current language. But it all depends on.. whether you were the first one to request the error messages. As far as we could detect, WFFM caches the error messages but forgot to take the language into account when caching...

So, how do we fix this? Luckily there is a way out. In the documentation of wffm 2.x the "Custom Errors" are mentioned and they are still present in 8.1...

Custom errors

Custom errors are items that can be used as error messages. A few ones exist and you can create more if needed. We used the available custom required and created a new custom email to test.
The items have a key: just look at the regular error message that you would like to use and copy the key (e.g. DynamicRequiredAttribute or DynamicEmailAttribute). In the "value" field you enter your custom message. 
Now create versions for all languages and translate the message as needed.

Using the custom errors

After creating custom error messages, we need to tell our forms to use them. Unfortunately this needs to be done for every field...


In the content editor go to your (required) field. Find the "Dictionary" section and select the desired entries from the list of all custom errors. In many cases that will just be the Custom required, but on an email field you can select for example Custom required and Custom email. Once you've done this for all your fields you're good to go.

Just don't forget to publish all your changes... 

Translating WFFM 8.1 MVC forms error messages part I

Multilingual WFFM

WebForms for Marketers has quite some issues challenges with multilingualism in version 8.1 but in this post I want to focus on the error messages. Error messages come in 2 flavors: client-side and server-side. This first post will handle the client-side validation - server side is for part II.

Client-side validation and error messages

For client-side validation we encountered several issues. First of all, we could not get the messages multi-lingual. Secondly, they didn't work for some fields (e.g. the checkbox list). So the idea we came up with is simple: we remove the client-side validation. 

Removing the client-side validation

First thing to do is disable "Is Ajax MVC Form". Normally this should be sufficient. And it will.. on the first request. But once your visitor gets a validation error (from the server-side validation) the form is reloaded and the client-side validation is back. 

So, let's go for some more serious enhancements then. We'll get rid of the javascript performing the validation!

In "sitecore modules\Web\Web Forms for Marketers\mvc" you will find (amongst others) 2 javascript files: main.js and wffm.js. 

Main.js

  • Remove "jquery_validate: "sitecore%20modules/Web/Web%20Forms%20for%20Marketers/mvc/libs/jquery/jquery.validate.min",
  • Remove "jquery_validate_unobtrusive: "sitecore%20modules/Web/Web%20Forms%20for%20Marketers/mvc/libs/jquery/jquery.validate.unobtrusive.min",
  • Edit the "shim" part by removing all references to jquery_validate and jquery_validate_unobtrusive
This should result in:

...
require.config(
{
  baseUrl: generateBaseUrl(),
  paths: {
   jquery: "sitecore%20modules/Web/Web%20Forms%20for%20Marketers/mvc/libs/jquery/jquery-2.1.3.min",
   jquery_ui: "sitecore%20modules/Web/Web%20Forms%20for%20Marketers/mvc/libs/jquery/jquery-ui-1.11.3.min",
   bootstrap: "sitecore%20modules/Web/Web%20Forms%20for%20Marketers/mvc/libs/bootstrap/bootstrap.min",
   wffm: "sitecore%20modules/Web/Web%20Forms%20for%20Marketers/mvc/wffm.min"
  },
  waitSeconds: 200,
  shim: {
    "bootstrap": {
      deps: ["jquery"]
    },
    "jquery_ui": {
      deps: ["jquery"]
    },
    "wffm": {
      deps: ["jquery", "jquery_ui", "bootstrap"]
    }
  }
});
...

Wffm.js

In wffm.js there are several lines referring to the validation which will show errors in your browsers console when you do not remove them. So we started cleaning up.. posting the whole resulting javascript file would be a bit too long but if you take these steps you will get there:

  • remove the "if (ajaxForm)" part
  • remove the $scw.validator.setDefaults part
  • search for validator and remove the references
  • remove any now unused functions 

And last but not least: as you could see in main.js, the minified version of wff.js is used so you will need to minify your changed version and overwrite wffm.min.js.


Sunday, February 14, 2016

Integrating AddThis with Sitecore goals

Social buttons


We faced the requirement to add social buttons to a Sitecore 8.1 site and track the interaction with a goal in xDB. Oh yes, and do it fast.

Social Connected

Our first thought was using the social buttons provided by Sitecore (in 'social connected'). But, we bumped into some issues (well, actually just not enough options) and had to write quite some button-code ourselves.

AddThis

So we started looking for alternatives and got back to a tool which we had used before and was known to our customer : AddThis. Problem was that it is not integrated within Sitecore and therefor did not create a goal. After discussing pros and cons we went for the flexibility of AddThis and started writing the code to trigger a goal.

Trigger a goal in a MVC controller

Some people have already blogged about not being able to get to the current Tracker from within WebApi ..  With SSC (Sitecore.Services.Client) it should be possible - as Mike Robbins has shown:


but as we did not have that information yet and we could not afford to take any risks, we went for the solution that seemed easy: create a mvc controller and a HttpPost action.

The controller action


[HttpPost]
public ActionResult TriggerGoal(string goal)
{
 if (!Tracker.IsActive)
 {
  return Json(new { Success = false, Error = "Tracker not active" });
 }

 if (string.IsNullOrEmpty(goal))
 {
  return Json(new { Success = false, Error = "Goal not set" });
 }

 var goalItem = ... // get goal item from Sitecore based on goal string
 if (goalItem == null)
 {
  return Json(new { Success = false, Error = "Goal not found" });
 }

 var visit = Tracker.Current;
 if (visit == null)
 {
  return Json(new { Success = false, Error = "Current tracker is null" });
 }

 var page = Tracker.Current.Session.Interaction.PreviousPage;
 if (page == null)
 {
  return Json(new { Success = false, Error = "Page is null" });
 }

 var registerTheGoal = new PageEventItem(goalItem);
 var eventData = page.Register(registerTheGoal);
 eventData.Data = goalItem["Description"];
 eventData.ItemId = goalItem.ID.Guid;
 eventData.DataKey = goalItem.Paths.Path;
 Tracker.Current.Interaction.AcceptModifications();

 Tracker.Current.CurrentPage.Cancel(); // Abandon current request so that it doesn't appear in reports

 return Json(new { Success = true });
}

We try to find the goal (Sitecore item) based on the post parameter. If found, we use that data to register a PageEvent to the PreviousPage of the current Tracker. We end by 'saving' the changes and cancelling the current request. The return values are an indication of what happened.

Remember:  Do not forget to register the route to your controller... ;)


The javascript

(function(global){

 var jQuery = global.jQuery;
 var addthisComp = {
  $instance: null,
  
  init: function(){
   addthisComp.$instance = jQuery('.addthis_sharing_toolbox');
   if(addthisComp.$instance.length){
    
    // Listen for the ready event
    if(global.addthis){
     if(global.addthis.addEventListener) 
          global.addthis.addEventListener('addthis.menu.share', addthisComp._share);
    }

   }
  },

  // Dispatched when the user shares
  _share: function(evt) {
      jQuery.ajax({
    type: "POST",
    url: '...', // url - route to your controller action
    data: {"goal": evt.data.service},
    success: function(obj){ 
    },
    dataType: 'json'
   });
  }
 };

 global.addthisComp = addthisComp;
   jQuery(document).ready(function(){
     global.addthisComp.init();
 });

})(window);

This is just attaching an ajax post request to out controller when the share functions of addthis are used. Of course, the post action could be attached to any event you want..


Sunday, January 24, 2016

Sitecore 8.1 encode name replacements

<encodeNameReplacements>

In Sitecore 8.1 you will find an extra entry in the <encodeNameReplacements> section of the Sitecore config (now located in /App_Config/Sitecore.config): 
<replace mode="on" find=" " replaceWith="-" />

This addition will replace all spaces in the generated urls (so either in display name or name) with a '-'.  Looks very nice, our seo-friends happy, but...

Two issues have been noticed which you might need to tell your editors about:

1. Items with a " " and a "-" in the (display) name

Our editors created several items with both characters in the name.. they all resulted in a 404 page because the item could not be resolved by Sitecore. Two ways to handle this:
- turn off the replacement
- tell your editors not to do that (optionally create a rule with a regular expression to validate item names)

We went for option 2..  


2. Wildcard items

When you have a wildcard item in a folder (item named "*") this replacement will give additional issues..  As soon as you create an item with a space in it, it will not be found and directed to the wildcard instead. Apparently the check for wildcards is done before the handling of replacements which causes this behavior. 

There is a support fix available for this (452602 - patching the ItemResolver), but another (and maybe easier) way of handling it is not using spaces in folders with wildcard items.


That is for now - tell your editors to avoid getting (a lot of) bug reports for broken urls ;)

Update

Apparently the issue(s) has been fixed in Sitecore 8.1 Update 2 (see http://sitecorefootsteps.blogspot.be/2016/03/item-url-replacement-improvements.html)