Friday, November 13, 2015

Sitecore indexes

There are already a lot of blog posts describing the use of Sitecore indexes, especially since Sitecore 7 and the introduction of the ContentSearchManager and the ease to use them.
And still.. I see lots of people writing queries going through lots of items. So: yet another post to promote the use of indexes.

Sitecore indexes, indexes, indexes!

Sitecore has some built-in indexes (since Sitecore 8 even more). The best know are probably the sitecore_master and sitecore_web indexes. Personally I never use those. I always create a custom index. Why? 
  • I don't want to mess with the indexes that Sitecore uses
  • I want my indexes small and lean
    • faster (re)build
    • easier to check

When to use?

It's hard to say exactly when to use an index, but I'll try to give some common real-life examples of request where I almost always think "index":
  • fetch all news items from year x
  • fetch all products from category x
  • fetch all events happening in the future
  • fetch the latest news items
  • get the last 3 blog posts written by x
  • ...
Too often developers write queries which are fast with the test data.. but after a while the real data is has outgrown the solution and it gets slow..  So it's better to think ahead and make more use of those indexes. In lots of cases the result will be faster than a (fast) query.

Create a custom index

As there already are a lot of examples out there, just a fast introduction on how you can create (and use) a custom index. For more information on all the possibilities, check the Sitecore docs (or the default index config files which include examples and comments).

Configuration

I usually create a separate config file where I put the index definition and configuration together. 

Example index definition:

<configuration type="Sitecore.ContentSearch.ContentSearchConfiguration, Sitecore.ContentSearch">
  <indexes hint="list:AddIndex">
    <index id="MyCustom_index" type="Sitecore.ContentSearch.LuceneProvider.LuceneIndex, Sitecore.ContentSearch.LuceneProvider">
      <param desc="name">$(id)</param>
      <param desc="folder">$(id)</param>
      <!-- This initializes index property store. Id has to be set to the index id -->
      <param desc="propertyStore" ref="contentSearch/indexConfigurations/databasePropertyStore" param1="$(id)" />
      <configuration ref="contentSearch/indexConfigurations/myCustomIndexConfiguration" />
      <strategies hint="list:AddStrategy">
       <!-- NOTE: order of these is controls the execution order -->
       <strategy ref="contentSearch/indexConfigurations/indexUpdateStrategies/onPublishEndAsync" />
      </strategies>
      <commitPolicyExecutor type="Sitecore.ContentSearch.CommitPolicyExecutor, Sitecore.ContentSearch">
        <policies hint="list:AddCommitPolicy">
          <policy type="Sitecore.ContentSearch.TimeIntervalCommitPolicy, Sitecore.ContentSearch" />
        </policies>
      </commitPolicyExecutor>
      <locations hint="list:AddCrawler">
        <crawler type="Sitecore.ContentSearch.SitecoreItemCrawler, Sitecore.ContentSearch">
         <Database>web</Database>
           <Root>/sitecore/content/Corporate</Root>
        </crawler>
      </locations>
    </index>
  </indexes>
</configuration>

In this example I used the LuceneProvider, the onPublishEndAsync update strategy (info on update strategies by John West here) and refer to my custom configuration.
Note that I added a crawler for the web database and gave it a root path (can also be an ID).


Example index configuration:

<indexConfigurations>
  <myCustomIndexConfiguration type="Sitecore.ContentSearch.LuceneProvider.LuceneIndexConfiguration, Sitecore.ContentSearch.LuceneProvider">
    <indexAllFields>true</indexAllFields>
    <initializeOnAdd>true</initializeOnAdd>
    <analyzer ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/analyzer" />
    <documentBuilderType>Sitecore.ContentSearch.LuceneProvider.LuceneDocumentBuilder, Sitecore.ContentSearch.LuceneProvider</documentBuilderType>
    <fieldMap type="Sitecore.ContentSearch.FieldMap, Sitecore.ContentSearch">
      <fieldNames hint="raw:AddFieldByFieldName">
        <field fieldName="_uniqueid" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
          <analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
        </field>
        <field fieldName="__sortorder" storageType="YES" indexType="UNTOKENIZED" vectorType="NO" boost="1f" type="System.Integer" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider"/>
        <field fieldName="title" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
          <analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
        </field>
        <field fieldName="date" storageType="YES" indexType="UNTOKENIZED" vectorType="NO" boost="1f" type="System.DateTime" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider"/>
        <field fieldName="sequence" storageType="YES" indexType="UNTOKENIZED" vectorType="NO" boost="1f" type="System.Integer" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider"/>
        <field fieldName="topics" storageType="YES" indexType="UNTOKENIZED" vectorType="NO" boost="1f" type="System.Guid" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider"/>
        <field fieldName="applications" storageType="YES" indexType="UNTOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider"/>
      </fieldNames>
    </fieldMap>
    <include hint="list:IncludeTemplate">
      <NewsTemplate>{5CD362B8-C129-437A-A0D4-4EE58E71FEB1}</NewsTemplate>
      <ProductTemplate>{18D5467C-79F9-405B-AA87-2BA4B7CDB443}</ProductTemplate>
      <EventTemplate>{6CA9AC2A-1A9D-429B-870C-FC9417D3A1C7}</EventTemplate>
    </include>
    <fieldReaders ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/fieldReaders"/>
    <indexFieldStorageValueFormatter ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/indexFieldStorageValueFormatter"/>
    <indexDocumentPropertyMapper ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/indexDocumentPropertyMapper"/>
  </myCustomIndexConfiguration>
</indexConfigurations>


Notice here:

  • the "indexAllFields" : if not true, you need to define the fields in an include (just like the IncludeTemplate, but then with includeField)
  • the "fieldMap": here we define the field options: storageType, type (can be string, int, guid, date, ...) and if needed an analyzer (all information on analyzers by Adam Conn here)
  • the IncludeTemplate section where we define the templates of the items to include in the index (the guid is important, the name is useful for understanding the config)

Standard Sitecore fields
Most of the standard Sitecore fields are included automatically. 
Some are not, but you can include them (in the example above the SortOrder field is included).


After creating the configuration you should see your index in the Index Manager in Sitecore and you can rebuild it. Check your data in the index with a tool like Luke. This way you are sure your config is good before you start using it. Luke is also handy later on to check your queries.

Computed fields

Computed fields are fields that are added to the index through custom code (the value is computed instead of just fetched from a field). Off course, computed fields can also be added to a custom index.

Querying your index

Sitecore has a class SearchResultItem that can be used to fetch results from the index, but in most cases you will want to extend this class.

Example SearchResultItem:


public class EventItem : SearchResultItem
public class EventItem : SearchResultItem
{
  [IndexField("title")]
  public string Title { get; set; }
  
  [IndexField("startdate")]
  public DateTime StartDate { get; set; }
  
  [IndexField("profile")]
  public ID Profile { get; set; }
}
We use the SearchContext to do the actual query. Example code:

private IEnumerable<EventItem> GetEventItems()
{
  var templateRestrictions = new List<ID>
  {
    new ID(applicationSettings.EventsTemplateId)
  };

  using (var context = ContentSearchManager.GetIndex("MyCustom_index").CreateSearchContext())
  {
    var templatePredicate = PredicateBuilder.False<EventItem>();
    templatePredicate = templateRestrictions.Aggregate(templatePredicate, (current, template) => current.Or(p => p.TemplateId == template));
    var datePredicate = PredicateBuilder.True<EventItem>();
    datePredicate = datePredicate.And(p => p.StartDate >= DateTime.Today);
    var predicate = PredicateBuilder.True<EventItem>();
    predicate = predicate.And(templatePredicate);
    predicate = predicate.And(datePredicate);
    predicate = predicate.And(p => p.Language == Sitecore.Context.Language.Name);
    var query = context.GetQueryable<EventItem>(new CultureExecutionContext(Sitecore.Context.Language.CultureInfo)).Where(predicate).OrderBy(p => p.StartDate);
    var queryResults = query.GetResults();
    foreach (var hit in queryResults.Hits)
    {
      if (string.IsNullOrEmpty(hit.Document.Title))
      {
        continue;
      }

      yield return hit.Document;
    }
  }
}
We use "predicates" to define our query. I find them useful to create reusable code (not shown here), especially combined with generics. Predicates are created with the PredicateBuilder (use true for "and" and false for "or" queries).

First we defined a predicate to check the templateID (from a list of possibilities). We also check a datefield and in the end we have predicate for the language.

In the example we sort (OrderBy), but the queryable has also options to use paging, facetting, ... The resultSet include a list of results, but also the facets, the total number of results (important when paging), ..   

Make sure that if you sort you are using the correct types. Sorting numbers as string will give you unexpected results..

Fetching Sitecore items
It is also important to know that the results are not yet Sitecore items - we get the items we define (our SearchResultItem's). It is however quite easy to fetch the actual Sitecore items here, also using Glass if you want. Be aware though that the part after the index is sometimes the performance bottleneck: you wouldn't be the first to lose all performance benefits from the index by fetching too many Sitecore items or writing a slow Linq query after the search.

Before fetching the real Sitecore items (or Glass-mapped-classes), consider if you really need them. In lots of cases you will, but sometimes the information from the index can be sufficient and you can save even more time not retrieving actual items.


Logs

If your query returns unexpected results a good place to start looking in the search log file. All queries that are performed are logged there and if you are using Luke you can copy/paste the query in Luke and test it. 


Issues

There are some known issues.. some unknown as well. I have a few open tickets with Sitecore support regarding indexes at the moment, so maybe more posts will follow...

Thursday, November 12, 2015

Delivering instant data to a high traffic Sitecore site

The challenge


We had to deliver data to 12 to 15.000 concurrent users on 2 Azure servers running on Sitecore (7).
Most of the data was coming from an external source (restful services) and could (should) not be cached for longer than 1 second because it was real time (sports) scoring information. That data was merged with extra information from Sitecore.

In this post I will describe what we did to achieve this, knowing that we could not "just add some servers". We had an architecture with 2 content delivery servers, 1 content management server and a database server. That could not be changed.

All things described here had some impact, some more than others or on different levels of the application but I hope it might give you some ideas when facing a similar challenge. The solution was build on Sitecore 7 with webforms and webapi.


Coding

Using context classes

Our most visited pages had up to 10 controls showing data (so not counting controls for creating grids and so). All of these controls needed a same set of data fetched from the url, page, ...  The worst solution would be to have all that logic in every control. A better (and actually faster) way could have been to put that logic in a class and use that as base class or call it from all controls.
But in order to prevent all these controls going through that process and possible doing request to back-end services we took another approach and created a "context" class, injected per request (using Autofac in our case). The "context" class was called before any control was initialized and prepared all necessary context data for the page - without having to know what controls are on the page because that would break the whole idea of Sitecore renderings.

Example
We had a "team pages". Each team had several pages with different controls on them (player list, statistics, coach info, pictures ... ) and as it should in a Sitecore environment the editors decide what controls they want on each page. But: on each team page we need to know the team and we could already fetch the global team information as this was one object in the back-end systems. Depending on the control this could cover up to 50% of the data needed, but for each control it was one less request. If you have 10 controls on a page, this matters.. (even if the data would be cached).


Caching

Some of the data from Sitecore could be cached. At least, until it was changed in Sitecore. There are lots of post already out there on how to clear your cache when a Sitecore publish is done, but then you might clear your cache too often. So we created a more granular caching system. Maybe I should write a blog post on this one alone but in a nutshell it comes down to this: each entry in the cache has a delegate that determines whether the cache should be cleared (and maybe even immediately refilled) and after Sitecore publish, for each cache entry the delegate is called. 

This way each cache entry is responsible for clearing itself. In the delegate we can check language, published item, ..  If a lot of Sitecore publishes are happening this mechanism can prevent quite some unneeded cache clearances. Of course, one badly written delegate method could kill your publish performance, but if you keep them simple (try to get out of the method as soon as possible) it's worth it.


Threads

As mentioned before we could not cache the data coming from the external back-end system for any longer than 1 second. The API to the system was build with restful services. Calling the services when we needed the data was not an option if we wanted to serve that many users. Our solution was threading. We created a first long running thread that called the back-end every 5 minutes to see whether there was data to be fetched (this timing was fine as data was starting to show at least a day before the actual live games started). When we detected live data coming in we would start a new thread that fetched the actual data constantly and kept it in memory available for the whole application (until we detected the end of the data stream and let the thread stop). With the constant loop that fetched the data, mapped it to our own business objects (Automapper to the rescue) and sometimes even performed some business logic on it, we were able to keep the "freshness" of the data always under the required 1 second.

So the data was available on our site -in memory- at all time and we the threads for the web application were not harmed as the retrieval itself had it's own threads. A monitoring system was put in place to detect the status of the running threads and we included quite some logging to know what was going on, but in the end it all went well and was stunningly fast.


WebAPI / AngularJS

To deliver the live data on the pages we used AngularJS and WebApi to change the data on the pages without extra page request. For some of the controls this also enabled us to provide at least some content immediately to the users while fetching the rest.
Other tricks like progressive loading of images managed to get the amount of data that is initially loaded by the page down to a minimum.


Tuning

Pagespeed optimizations

This is something you should consider on every site no matter what the traffic will be like. I am talking about bundling and minifying css and javascript, optimizing images (for Sitecore images, use the parameters to define width and/or height), enabling gzip compressing and so on.. Mostly quite easy tasks that can give your site that extra boost.

IIS tuning

Probably a bit less know and for many sites not necessary, but also your IIS webservice can be tuned.
There is a good article by Stuart Brierly on the topic here.
What we did in particular is change the configs to adapt to the number of processors allowing more threads and connections. When doing this you need to make sure off course that your application can handle those extra simultaneous requests. 
We also adapted the number of connections that were allowed to the external webservice (by allowing more parallel connections to the IP-address).

These changes made sure that IIS did not put connections on hold while we still had some resources left on the server.

This looks like a small change, but with quite some impact and you will need to perform some load tests to test your changes.


Infrastructure

Servers

As said in the beginning we could not add more servers, but we did upscale the servers as far as we could.

Caching

The last step to the solution was adding caching servers (Varnish) in front of the solution. This could free the webservers from a lot of request that could easily be cached. Here the use of WebApi to load some data also helped to get quite some requests cached: this can be resources like javascript files or images, but also complete pages. If you can serve these request without them going all the way to your webserver, your server has a smaller request queue and more resources to handle the remaining requests. 

This last step does not come for free, but it had a huge impact once configured properly.

Wednesday, August 5, 2015

Sitecore wildcard items to the max

Wildcard items

There are enough blogs already describing what a wildcard item in Sitecore is, so a very short intro should do here: 
A Sitecore wildcard item is a regular item in Sitecore with "*" as name. The item has all the fields of a normal item.
The item will match on any url in its tree on its level that is not matched on any of its siblings. Wildcard items can have children.
Some Sitecore versions have a bug when using a display name in the path up to a wildcard item (hotfix available).


The case

We had a case for a sports organisation with a few requirements:
  • create a website for a competition that can host a few overall pages and 'subsites' for all 20 events in the competition
  • editors do not want to create 20 trees in Sitecore
  • all data about the competitions comes live from a backend system and is not copied into Sitecore
  • the competition is yearly so the site needs to be ready to be copied next year
  • each event has around 120 matches 
  • each event can have a mens and/or a womens tournament, each with around 60 competitors

The solution

We used some wildcards..  
The first wildcard is right below the general home page, and defines a sub-homepage for the event. Underneath we have the complete set of items for each event. 


More wildcards

Even more wildcards appear. In the (wo)mens section we have a wildcard to display a team, and underneath another one to display a player. This way we can have pages (seo friendly) for all teams and players in each event.

We also have wildcards for each game (under that wildcard we have subitems for the subpages of a match).

Navigation

The navigation is in 2 parts: first of all we have navigation that displays all events. That is not Sitecore related (data comes from backend system) but will point to the wildcard item under the home item - off course indicating the right event in the url.

The navigation inside an event can be set once. The links are automatically transferred into the correct url for that event (see part about code). Some pages will not have content, but that is handled on the page itself.

Content 

Sitecore content is shown based on datasources - as it should. But here the datasource will not go to a specific item but to an item container. In that container we look for content that can be displayed for the current event. If that is not found we display a message that the content is not yet available.

The code

Events and tournaments have a unique code. That code will be used in the urls on the spot of the main wildcard. We will use the eventcode as much as possible, as soon as a selection is made for the men/women tournament, we use the tournament code. That is because we need the correct code to go to the backend system.

To accomplish this we wrote a context class (instance per request) that detects the "context" - the current event and or tournament - from the url and exposes this to everyone who needs it. 

LinkProvider


The nifty stuff is in our custom LinkProvider. We wrote some code that determines the requested tournamentcode, applying some business rules and based on the current context. In the end, we replace the first wildcard in the url with the determined code (only the first one, as we might have multiple) with string manipulation.

Other wildcards

The other wildcards (team, player, match) are handled in their business objects. Each business class has it's own logic to create the urls and grab the information needed back from the url. As all our code passes through the business objects and nothing is done straight on the base Sitecore items, we are good here. Off course, it's not possible to put a url to a team or so from within the content but that was not a requirement anyway (actually it is possible as a 'external' url).

Components

The event homepage contains several components that need to be activated based on the status of the event (not started, ongoing, finished). This is also done automatically based on the event in our context. All components are placed on the page and the ones that are not compliant will be hidden.
We could have used a custom condition here and use the standard hide functionality to create conditional rendering rules, but we kept it simple and put the logic in a base function for all homepage components.

Conclusion

We had a first version (poc) quite fast but it took us a few days and a lot of testing to get everything working as it should - especially the small and tricky parts like the multilingual aspect, canonical urls, links between tournaments, links from within event components showing data about more than one tournament...

I did not put any of the code in here as it is quite specific based on our business logic and the backend system used, but if there are questions or you want to see some code or config - just ask ;)

Monday, October 20, 2014

Do NOT use Sitecore.Data.Database constructor

How to kill your website - part x

This post could indeed also be called "How to kill your website" because that is what using the constructor of the Sitecore.Data.Database class in Sitecore will do. 

If you set your log level to INFO and see something like this in the log files:

4184 08:30:17 INFO  Cache created: 'web[blobIDs]' (max size: 500KB, running total: 624MB)
4184 08:30:17 INFO  Cache created: 'web[blobIDs]' (max size: 500KB, running total: 624MB)
4184 08:30:17 INFO  Cache created: 'web[blobIDs]' (max size: 500KB, running total: 625MB)
4184 08:30:17 INFO  Cache created: 'web[blobIDs]' (max size: 500KB, running total: 625MB)
4184 08:30:17 INFO  Cache created: 'web[blobIDs]' (max size: 500KB, running total: 626MB)
4184 08:30:17 INFO  Cache created: 'web[blobIDs]' (max size: 500KB, running total: 626MB)
4184 08:30:17 INFO  Cache created: 'web[blobIDs]' (max size: 500KB, running total: 627MB)

This means you have a memory leak...  you're site is dying. Slowly (maybe), but surely.
Look into your code and see if you have ever used new Database(). And then get rid of that code.

How to fetch a database

The wrong way:

var database = new Database("web");


The right way(s):

var database = Sitecore.Data.Database.GetDatabase("web");
or
var database = Sitecore.Configuration.Factory.GetDatabase("web")

Thursday, October 16, 2014

Security in ComputedIndexFields

Custom ComputedIndexField

How to create a custom ComputedIndexField in Sitecore can be found on several other location on the net. But what if you want to check the security of your item when populating this field? 

A simple function could check if your item was readable by an anonymous user:
const string anonymous = @"extranet\Anonymous";
var account = Account.FromName(anonymous, AccountType.User);
return AuthorizationManager.IsAllowed(item, AccessRight.ItemRead, account);

This will work in a normal environment, but if you try this during population of your ComputedIndexField you will always get true. Why?

SitecoreItemCrawler

The responsible crawler - Sitecore.ContentSearch.SitecoreItemCrawler - is designed in such a way to wrap the calls for your ComputedIndexField in a SecurityDisabler. So, the true value in our security check is by design.

And maybe for the best.. it seems like a much better approach to perform security checks indexed results are being accessed. And how do I do this?

Security and index results


Well, you don't. Sitecore does this for you. When you query your index, Sitecore has a Security check build in to the pipeline:  <processor type="Sitecore.Pipelines.Search.SecurityResolver, Sitecore.Kernel" /> . This checks the security of the item involved, not of any related items that you might have used to fill your ComputedIndexField. If you also want a security check on those, write a processor to extend the pipeline :)

Tuesday, October 14, 2014

ComputedIndexField with extra attributes

ComputedIndexField attributes

In the code that populates my ComputedIndexField I needed a reference to a Sitecore item. I needed to know a "context" item from which I had t retrieve data. Apparently you can extend Sitecore to do this (off course).

My configuration look like this (in a separate config file):

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
 <sitecore>
  <contentSearch>
   <configuration type="Sitecore.ContentSearch.LuceneProvider.LuceneSearchConfiguration, 
                                  Sitecore.ContentSearch.LuceneProvider">
    <indexes hint="list:AddIndex">
     <index id="MyIndexName" 
         type="Sitecore.ContentSearch.LuceneProvider.LuceneIndex,Sitecore.ContentSearch.LuceneProvider">
      <configuration type="Demo.Search.LuceneIndexConfiguration, Demo">
       <documentOptions type="Sitecore.ContentSearch.LuceneProvider.LuceneDocumentBuilderOptions">
        <ProcessDependencies>true</ProcessDependencies>
       </documentOptions>
       <fields hint="raw:AddComputedIndexField">
        <field fieldName="DemoContent" label="{1174978C-47CA-405D-13FE-4C808F3A85E7}">
          Demo.Search.DemoField, Demo
        </field>
       </fields>
      </configuration>
     </index>
    </indexes>
   </configuration>
  </contentSearch>
 </sitecore>
</configuration>

 So I added an attribute "label" in my field definition. Now we need a custom IndexConfiguration, and we will start with the one from Sitecore (reverse engineer that one and you will see we only added a few lines).

Custom IndexConfigration

public class LuceneIndexConfiguration : Sitecore.ContentSearch.LuceneProvider.LuceneIndexConfiguration
{
  public override void AddComputedIndexField(XmlNode configNode)
  {
    Assert.ArgumentNotNull(configNode, "configNode");
    string fieldName = XmlUtil.GetAttribute("fieldName", configNode, true);
    string type = XmlUtil.GetValue(configNode);            
    if (string.IsNullOrEmpty(fieldName) || string.IsNullOrEmpty(type))
    {
      throw new InvalidOperationException("Could not parse computed index field entry: " + configNode.OuterXml);
    }
    var field = ReflectionUtil.CreateObject(type) as IComputedIndexField;

    // here starts custom code
    var constructableField = field as IConstructable;
    if (constructableField != null)
    {
     constructableField.Constructed(configNode);
    }
    // end custom code

    if (field != null)
    {
      field.FieldName = fieldName.ToLowerInvariant();
      DocumentOptions.ComputedIndexFields.Add(field);
    }
  }
}

The extra stuff is based on the IConstuctable interface. This allows you to created a Constructed method that can read your xml node. You can use your own interfaces as well surely, but this one is already in the box..

And then we adapt the code for our ComputedIndexField:

IConstructable

public class DemoField : Sitecore.ContentSearch.ComputedFields.IComputedIndexField, IConstructable
{
  private Item MyItem { get; set; }

  public void Constructed(XmlNode configuration)
  {
    var label = XmlUtil.GetAttribute("label", configuration, true);
    if (string.IsNullOrEmpty(label))
    {
      ....
      return;
    }

    Guid id;
    if (Guid.TryParse(label, out id))
    {
      MyItem = Factory.GetDatabase("master").GetItem(new ID(id));
    }
  }

  public object ComputeFieldValue(IIndexable indexable)
  {
    ...
  }
}

We implement the Constructed method and read the xml node. The GetAttribute function is very useful here. We set the MyItem that will be used later on in the ComputedFieldValue function called by the crawler.

Monday, October 13, 2014

Multiple datasources for Associated Content

Associated Content

In the definition of a sublayout in Sitecore you can select a Datasource location and a Datasource template. This makes it easier for your editors to pick the right datasource when using this sublayout. But what if you want to allow more than one datasource template? The answer was found somewhere well hidden on the Sitecore developers site:


Please refer to the following Release note, which describes the officially recommended way to implement such behavior: http://sdn.sitecore.net/Products/Sitecore%20V5/Sitecore%20CMS%206/ReleaseNotes/ChangeLog.aspx
The "Select Associated Content" dialog has been made more extensible. (339396)
Developers can now specify multiple allowed templates by hooking into the <getRenderingDatasource> pipeline and populating the new TemplatesForSelection property.
The default behavior is to use value of the "Datasource Template" field on the rendering item.

How to use multiple datasource templates?


So, all you need to do is create a config file and place it in the include folder (do not alter the web.config):

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
 <sitecore>
   <!-- PIPELINES -->
   <pipelines>
     <getRenderingDatasource>
      <processor patch:after="*[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceTemplate, 
              Sitecore.Kernel']" type="Demo.SitecorePipelines.GetMultipleDataSourceTemplates, Demo"/>
     </getRenderingDatasource>
   </pipelines>
 </sitecore>
</configuration>


and create your class:

public class GetMultipleDataSourceTemplates
{
    public void Process(GetRenderingDatasourceArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
        if (args.Prototype != null)
        {
            return;
        }

        var data = args.RenderingItem["Datasource Template"];
        if (string.IsNullOrEmpty(data))
        {
            return;
        }

        var templates = data.Split('|');
        foreach (var name in templates)
        {
            var item = args.ContentDatabase.GetItem(name);
            if (item == null)
            {
                continue;
            }
            var template = TemplateManager.GetTemplate(item.ID, args.ContentDatabase);
            if (template != null)
            {
                args.TemplatesForSelection.Add(template);
            }
        }
    }
}

Note that we use "|" as separator here. That is just as an example.
Ok, that's it. Now we can use it like this in Sitecore:



When saving your sublayout item, Sitecore will complain because the field in the datasource template cannot be linked to an item. Don't worry about this. Your editors won't notice and it works.

Template updates

One drawback is that Sitecore is not able to link the template to the field anymore. This means that if you move or rename the template the field will not be updated (as a normal datasource template field will).