Rendering Experience Editor Compatible Links Using Glass Mapper and Dynamically Built Properties

I encountered a problem when reviewing some legacy code using Glass Mapper v4.3.  Glass Mapper no longer supports dynamically build properties, while in Sitecore Experience Editor “Edit” mode.

View renderings had expressions like this:

using (BeginRenderLink(Model.GetListHeader(sList), x => x, isEditable: true))
{
   @Html.Raw(Model.GetListHeader(sList).Text);
}

The supporting Model class had the following:

[SitecoreField("List1_Header")]
public virtual Link List1Header { get; set; }

[SitecoreField("List2_Header")]
public virtual Link List2Header { get; set; }

[SitecoreField("List3_Header")]
public virtual Link List3Header { get; set; }

[SitecoreField("List4_Header")]
public virtual Link List4Header { get; set; }

public virtual Link GetListHeader(string sRenderingParameter)
{
   Link oReturn = null;
   int iList = 0;

   iList = Int32.Parse(sRenderingParameter);

   switch (iList)
   {
      case 1:
         oReturn = List1Header;
         break;
      case 2:
         oReturn = List2Header;
         break;
      case 3:
         oReturn = List3Header;
         break;
      case 4:
         oReturn = List4Header;
         break;
      default:
         break;
   }
   return oReturn;
}

While in Experience Editor Edit mode, the following exception was thrown:

Expression doesn't evaluate to a member x
...
at Glass.Mapper.Utilities.GetTargetObjectOfLamba[T](Expression`1 field, T model, MemberExpression& memberExpression) at Glass.Mapper.Sc.GlassHtml.MakeEditable[T](Expression`1 field, Expression`1 standardOutput, T model, Object parameters, Context context, Database database, TextWriter writer)

I did some digging on the web and found a few other references to this issue:

https://stackoverflow.com/questions/18698668/exception-in-glass-mapper-for-sitecore-in-pageeditor-mode

https://stackoverflow.com/questions/44059614/can-i-use-glassmappers-editable-on-a-reflected-property

Glass Mapper checks to make sure the lambda expression is a MemberExpression, and throws an exception when it’s not.  This is necessary due to updates in newer versions of Glass Mapper.

Here is what I did to work around this issue.

Remove the Dynamic Property References?

I could refactor the View rendering, adding an endless number of if statements, effectively pulling the model code into the View rendering, so that the property is statically referenced. That would create an even bigger mess.  This wasn’t going to work for me.  No thank you.

unhappy

Building a Dynamic MemberExpression in a Helper Method

After doing some reading about lambda expressions and coming across this question on Stack Overflow, I added the following method to the model class:

public virtual Expression<Func<T, object>> GetListHeaderExp(string sRenderingParameter)
{
   Expression<Func<T, object>> oReturn = null;
   int iList = 0;

   iList = Int32.Parse(sRenderingParameter);
   PropertyInfo propertyInfo = null;

   switch (iList)
   {
      case 1:
         propertyInfo = typeof(GenericPage).GetProperty("List1Header");
         break;
      case 2:
         propertyInfo = typeof(GenericPage).GetProperty("List2Header");
         break;
      case 3:
         propertyInfo = typeof(GenericPage).GetProperty("List3Header");
         break;
      case 4:
         propertyInfo = typeof(GenericPage).GetProperty("List4Header");
         break;
      default:
         break;
   }

   var entityParam = Expression.Parameter(typeof(GenericPage), "e");
   Expression columnExpr = Expression.Property(entityParam, propertyInfo);

   if (propertyInfo.PropertyType != typeof(Link))
      columnExpr = Expression.Convert(columnExpr, typeof(Link));

   oReturn = Expression.Lambda<Func<T, object>>(columnExpr, entityParam);

   return oReturn;
}

Call the MemberExpression Builder Method from the View

I then refactored the call in the View rendering to:

using (BeginRenderLink(Model, Model.GetListHeaderExp(sList), isEditable: true))
{
   @Html.Raw(Model.GetListHeader(sList).Text);
}

Conclusion

That did the trick.  I can now edit the link using the Sitecore Experience Editor, while keeping the dynamic property reference.

I’m hoping to write about my experience migrating an enterprise Sitecore installation from 7.2 to 8.2.  Hopefully, I’ll get to that soon.

Implementing Search Using Sitecore & Lucene – Part II

In the last post, I discussed aggregating data needed for search in a custom Lucene index. In this post, I’ll review how I implemented the query logic.

Date Conversion Exception

I started accessing the Lucene index and immediately started getting exceptions. Sitecore and Lucene were not happy with how my datetime data was getting stored in the Lucene index. I added a custom IndexFieldDateTimeConverter to manage the exceptions.


public class IndexFieldDateTimeValueConverter : Sitecore.ContentSearch.Converters.IndexFieldDateTimeValueConverter
{
    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
    {
        try
        {
             return base.ConvertFrom(context, culture, value);
        }
        catch(Exception e)
        {
             string fieldValue = value as string;

             DateTime dReturn = new DateTime();

             if (DateTime.TryParseExact(fieldValue, "yyyyMMdd", culture, DateTimeStyles.None, out dReturn))
                 return dReturn;
             else
                 throw e;
        }
    }
}

I created an include file to apply the class and had to name it with z (zCustomIndexValueConverters.config) so that it loaded after the Sitecore Content Search Lucene include files.


< configuration xmlns :patch ="http : / /www.sitecore.net /xmlconfig /" >
    < sitecore >
        < contentSearch >
            < indexConfigurations >
                < defaultLuceneIndexConfiguration >
                    < !-- DateTimeConverter -- >
                    < indexFieldStorageValueFormatter type ="Sitecore.ContentSearch.LuceneProvider.Converters.LuceneIndexFieldStorageValueFormatter, Sitecore.ContentSearch.LuceneProvider" >
                        < converters hint ="raw :AddConverter" >
                            < converter handlesType ="System.DateTime" >
                                < patch :attribute name ="typeConverter" >Someproject.ContentSearch.Converters.IndexFieldDateTimeValueConverter, Someproject<  /patch :attribute >
                            <  /converter >
                        <  /converters >
                    <  /indexFieldStorageValueFormatter >
                <  /defaultLuceneIndexConfiguration >
            <  /indexConfigurations >
        <  /contentSearch >
    <  /sitecore >
<  /configuration  >

I then applied the attribute to my data properties.

[TypeConverter(typeof(IndexFieldDateTimeValueConverter))]
 public virtual DateTime MetadataDate { get; set; }

Am I proud of myself? Nope. Did this work. Yep.

POCO and SearchResultItem

I needed classes to store the search results and facet data. I created four classes.

Facet Classes

The facet classes are fairly straight forward. The FacetValue class and the SearchFacet class are POCO (Plain Old CLR Objects) classes to store the facet data and return it to the presentation layer.

[Serializable]
[DataContract(Name = "FacetValue")]
public class FacetValue
{
    [DataMember(Name = "Value")]
    public string Value { get; set; }

    [DataMember(Name = "FacetCount")]
    public int FacetCount { get; set; }
}

[Serializable]
[DataContract(Name = "SearchFacet")]
public class SearchFacet
{
    private List _values;

    public SearchFacet()
    {
        _values = new List();
    }

    [DataMember(Name = "FacetName")]
    public string FacetName { get; set; }

    [DataMember(Name = "Values")]
    public List Values
    {
        get { return _values; }
        set { _values = value; }
    }
}

Search Results & Search Entity Classes

The search entity class stores all of the search result data we want to return to the presentation layer, as well as the properties to where and filter. I inherited from the Sitecore SearchResultItem class and then hid the data I did not want to return to the presentation layer for security and not to bloat the JSON. The search results class is the container for everything.

[Serializable]
[DataContract(Name = "SiteSearchEntity")]
public class SiteSearchEntity : SearchResultItem 
{
    [TypeConverter(typeof(IndexFieldIDValueConverter))]
    [IndexField("_id")]
    public Guid Id { get; set; }

    [DataMember(Name = "ComputedUrl")]
    [IndexField("LinkProviderUrl")]
    public virtual string ComputedUrl { get; set; }

    [DataMember(Name = "ComputedMetaTitle")]
    [IndexField("Title")]
    public virtual string ComputedMetaTitle { get; set; }

    [DataMember(Name = "ComputedMetaDescription")]
    [IndexField("Description")]
    public virtual string ComputedMetaDescription { get; set; }

    [IgnoreDataMember]
    [IndexField("Keywords")]
    public virtual string ComputedKeywords { get; set; }

    [DataMember(Name = "ComputedDocumentDate")]
    [IndexField("DocumentDate")]
    public virtual DateTime ComputedDocumentDate { get; set; }

    [DataMember(Name = "ComputedCategory")]
    [IndexField("ComputedCategory")]
    public virtual List ComputedCategory { get; set; }

    [DataMember(Name = "ComputedImageUrl")]
    [IndexField("ImageURL")]
    public virtual string ComputedImageUrl { get; set; }

    [DataMember(Name = "ComputedSearchUrl")]
    [IndexField("SearchURL")]
    public virtual string ComputedSearchUrl { get; set; }

    #region Hide Some Data Members
    [IgnoreDataMember]
    public new string Version { get; set; }

    [IgnoreDataMember]
    [IndexField("_group")]
    [TypeConverter(typeof(IndexFieldIDValueConverter))]
    public new ID ItemId { get; set; }

    [IgnoreDataMember]
    [IndexField("_uniqueid")]
    [TypeConverter(typeof(IndexFieldItemUriValueConverter))]
    [XmlIgnore]
    public new ItemUri Uri { get; set; }

    [IgnoreDataMember]
    [IndexField("_templatename")]
    public new string TemplateName { get; set; }

    [IgnoreDataMember]
    [IndexField("_template")]
    [TypeConverter(typeof(IndexFieldIDValueConverter))]
    public new ID TemplateId { get; set; }

    [IgnoreDataMember]
    [IndexField("__semantics")]
    [TypeConverter(typeof(IndexFieldEnumerableConverter))]
    public new IEnumerable Semantics { get; set; }

    [IgnoreDataMember]
    [IndexField("_fullpath")]
    public new string Path { get; set; }

    [IgnoreDataMember]
    [IndexField("_path")]
    [TypeConverter(typeof(IndexFieldEnumerableConverter))]
    public new IEnumerable Paths { get; set; }

    [IgnoreDataMember]
    [IndexField("_name")]
    public new string Name { get; set; }

    [IgnoreDataMember]
    [IndexField("_language")]
    public new string Language { get; set; }

    [IgnoreDataMember]
    [IndexField("__smallcreateddate")]
    public new DateTime CreatedDate { get; set; }

    [IgnoreDataMember]
    [IndexField("_content")]
    public new string Content { get; set; }

    [IgnoreDataMember]
    [IndexField("parsedcreatedby")]
    public new string CreatedBy { get; set; }

    [IgnoreDataMember]
    [IndexField("__smallupdateddate")]
    public new DateTime Updated { get; set; }

    [IgnoreDataMember]
    [IndexField("parsedupdatedby")]
    public new string UpdatedBy { get; set; }

    [IgnoreDataMember]
    [IndexField("_datasource")]
    public new string Datasource { get; set; }

    [IgnoreDataMember]
    [IndexField("_database")]
    public new string DatabaseName { get; set; }

    [IgnoreDataMember]
    [IndexField("_parent")]
    public new ID Parent { get; set; }

    [IgnoreDataMember]
    [IndexField("urllink")]
    public new string Url { get; set; }

    #endregion

    #region Work Around for Facets with Spaces
    [IndexField("CategoryFacet")]
    public virtual List CategoryFacet { get; set; }
    #endregion
}

[DataContract(Name = "SearchResults")]
[Serializable]
public class SerializableSearchResults
{
    List _entities = new List();
    List _facets = new List();

    [DataMember(Name = "TotalCount")]
    public int TotalCount { get; set; }

    [DataMember(Name = "SearchTerm")]
    public string SearchTerm { get; set; }

    [DataMember(Name = "entities")]
    public List entities
    {
        get { return _entities; }
        set { _entities = value; }
    }

    [DataMember(Name = "facets")]
    public List facets
    {
        get { return _facets; }
        set { _facets = value; }
    }
}

Search Logic & the Predicate Builder

Now for the fun part. How do we build a search algorithm to return accurate results?  I modeled my work after Matt Burke’s blog post.  I converted the search term into an array of strings, delimiting the term using the space character.  If then built the filter and term predicates separately and joined them together.   The Sitecore PredicateBuilder makes working Lucene fairly easy.  I simplified the search algorithm as it appears below for simplicity.

public static SerializableSearchResults GetSearchResultsLucene(string searchTerm, int page, Dictionary facets)
{
    SerializableSearchResults oReturn = new SerializableSearchResults();
    string sDatabase = "web";
    ISitecoreService service;

    ISearchIndex searchIndex = ContentSearchManager.GetIndex("sitesearch_web");

    SearchResults results = null;
    IQueryable query = null;

    service = new SitecoreService(sDatabase);

    using (IProviderSearchContext searchContext = searchIndex.CreateSearchContext())
    {
        // Parse search term into a collection of strings
        string[] terms = searchTerm.ToLower().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

        Expression filterPredicate = PredicateBuilder.True();

        // Get facets
        string category = GetFacetValue("category", facets);

        // Build facets clauses
        if (!String.IsNullOrEmpty(category))
            filterPredicate = filterPredicate.And(se => se.CategoryFacet.Contains(category));

        Expression termPredicate = PredicateBuilder.False();

        foreach (string term in terms)
        {
            termPredicate = termPredicate
                .Or(p => p.ComputedMetaTitle.Contains(searchTerm)).Boost(5.0f)
                .Or(p => p.ComputedMetaTitle.Like(term, 0.75f)).Boost(2.0f)
                .Or(p => p.ComputedMetaDescription.Contains(searchTerm)).Boost(3.0f)
                .Or(p => p.ComputedMetaDescription.Like(term, 0.75f)).Boost(2.0f)
                .Or(p => p.ComputedKeywords.Contains(searchTerm).Boost(2.5f))
                .Or(p => p.ComputedKeywords.Like(term, 0.75f).Boost(1.5f));
        }

        Expression fullPredicate = filterPredicate.And(termPredicate);

        query = searchContext.GetQueryable().Where(fullPredicate);

        FacetResults searchFacets = searchContext.GetQueryable().Filter(fullPredicate).FacetOn(x => x.CategoryFacet).GetFacets();

        query = query.Page(page - 1, 20);
        results = query.GetResults();
        oReturn.entities = results.Hits.Select(hit => hit.Document).ToList();

        foreach(SiteSearchEntity s in oReturn.entities)
        {
            service.Map(s);
        }

        oReturn.SearchTerm = searchTerm.ToLower();
        oReturn.TotalCount = results.TotalSearchResults;

        oReturn.facets = GetFacetResults(searchFacets);
    }

    return oReturn;
}

private static string GetFacetValue(string FacetName, Dictionary facets)
{
    string sReturn = String.Empty;

    if (facets.ContainsKey(FacetName))
        sReturn = facets[FacetName];

    return sReturn;
}

The method below loads the facet data into the POCO facet objects.

private static List GetFacetResults(FacetResults results)
{
    List f = new List();
    foreach (FacetCategory fc in results.Categories)
    {
        SearchFacet sf = new SearchFacet();
        sf.FacetName = fc.Name;
        foreach(Sitecore.ContentSearch.Linq.FacetValue fv in fc.Values)
        {
            sf.Values.Add(new Someproject.Models.Search.FacetValue() { FacetCount = fv.AggregateCount, Value = fv.Name });
        }
        f.Add(sf);
    }
    return f;
}

Paginated search results and the faceted breakdown of the search results are neatly packaged and are ready to be serialized into JSON for the presentation layer.

Conclusion

The Sitecore PredicateBuilder and Content Search Linq interface makes building a site search solution very managable.  Computed Fields allow the ability to store anything you need into the Lucene index file.

Implementing Search Using Sitecore & Lucene – Part I

Here is an overview of how I recently implemented search for a web site built using Sitecore.  I did not have the option to design the site templates from scratch.  Instead, I inherited a messy template inheritance structure with some inconsistencies in the design. I also had no budget, so Coveo was not an option.

poor

Index Creation

I started by creating a custom index to use for search.  I didn’t like the idea of tacking a large number of computed fields onto the default Sitecore indexes.

The search index needed to include items from the entire content tree as well as the media library.  I added two crawler location definitions.


< locations hint ="list:AddCrawler" >
    < crawler type ="Sitecore.ContentSearch.ExcludeItemCrawler, XL.Website" >
        < Database >master< /Database >
        < Root >/sitecore/content< /Root >
    < /crawler >
< /locations >
< locations hint ="list:AddCrawler" >
    < crawler type ="Sitecore.ContentSearch.ExcludeItemCrawler, XL.Website" >
        < Database >master< /Database >
        < Root >/sitecore/media library< /Root >
    < /crawler >
< /locations >

In the index configuration, I defined all of the templates that I needed to include in the index.


< include hint="list:IncludeTemplate" >
    < Product >{272C2195-AFE6-47CC-9707-BC8FB1909BE4}< /Product >
    < Article >{ABAEACC9-AC67-4A0D-B0DE-54982D2D3246}< /Article >
    ...
< /include >

I also defined any common fields that I would need in the index to search on, as well as display in the UI.


< fieldMap type ="Sitecore.ContentSearch.FieldMap, Sitecore.ContentSearch" >
    < fieldNames hint ="raw:AddFieldByFieldName" >
        < field fieldName ="MetadataTitle" storageType ="YES" indexType ="UNTOKENIZED" 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 ="MetadataKeywords" storageType ="YES" indexType ="UNTOKENIZED" 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 >
    < /fieldNames >
< /fieldMap >

Computed Fields

So, what about the data that is not stored consistently across the site?  You can aggregate the needed data using a computed field.

public class MetaDataDescriptionField : IComputedIndexField
{
   public string FieldName { get; set; }
   public string ReturnType { get; set; }

   public object ComputeFieldValue(IIndexable indexable)
   {
      Assert.ArgumentNotNull(indexable, "indexable");
      var indexableItem = indexable as SitecoreIndexableItem;

      if (indexableItem == null)
      {
         Log.Warn(string.Format("{0} : unsupported IIndexable type : {1}", this, indexable.GetType()), this);
         return null;
      }

      string sDescription = String.Empty;
      if (indexableItem.Item.IsDerived(indexableItem.Item, new Sitecore.Data.ID("some-template-guid")))
      {
         Sitecore.Data.Fields.Field stringField = indexableItem.Item.Fields["Somefieldname"];

         if (stringField != null)
         {
            sDescription = indexableItem.Item.Fields["Somefieldname"].Value;
         }
      }
      else
      {
          // Handle other templates ...
      }
      return sDescription;
   }
}

public static class ItemExtensions
{
        public static bool IsDerived([NotNull] this Item item, [NotNull] ID templateId)
        {
            return TemplateManager.GetTemplate(item).IsDerived(templateId);
        }
}

The computed fields get added to the custom Lucene index configuration.


< fields hint="raw:AddComputedIndexField" >
    < field fieldName="Description" storageType="YES" indexType="UNTOKENIZED" >Someproject.Indexes.Computed.MetadataDescriptionField, Somenamespace< /field >
    ...
< /fields >

I added a variety of computed fields.  Some of the fields accessed the LinkManager to store urls to pages or the MediaManager to store urls for images; needed by the presentation layer.  In addition, MultiList fields needed to be converted into a usable format so that they could be used for faceting.

Computed Fields for Facets

If you are using facet values in your presentation layer, rather than GUIDs, you will need to convert your Multilist fields into tokenized facet value data in the Lucene search index.

public class CategoryField : IComputedIndexField
{
   public string FieldName { get; set; }
   public string ReturnType { get; set; }

   public object ComputeFieldValue(IIndexable indexable)
   {
       Assert.ArgumentNotNull(indexable, "indexable");
       var indexableItem = indexable as SitecoreIndexableItem;

       if (indexableItem == null)
       {
           Log.Warn(string.Format("{0} : unsupported IIndexable type : {1}", this, indexable.GetType()), this);
          return null;
       }

       List sReturn = new List();

       if (indexableItem.Item != null)
       {
           if (indexableItem.Item.IsDerived(indexableItem.Item, new Sitecore.Data.ID("some-template-guid")))
           {
               Sitecore.Data.Fields.MultilistField multilistField = currentItem.Fields["somefieldname"];
               if (multilistField != null)
               {
                  sReturn = HelperClass.GetListValues(multilistField, "someotherfieldname");
               }
               else
               {
                ...
               }

               return sReturn;
           }
       }
}

public class HelperClass
{
   public static List GetListValues(MultilistField multiListField, string fieldName)
   {
       List results = new List();
       if (multiListField == null) { return results; }

       foreach (Sitecore.Data.ID sitecoreID in multiListField.TargetIDs)
       {
           Item sitecoreItem = SitecoreHelper.GetItem(sitecoreID.ToString());
           string result = SitecoreHelper.GetFieldValue(sitecoreItem, fieldName);
           if (!string.IsNullOrWhiteSpace(result))
           {
               results.Add(result);
           }
       }
       return results;
   }
}

< fields hint="raw:AddComputedIndexField" >
    < field fieldName="Description" storageType="YES" indexType="UNTOKENIZED" >Someproject.Indexes.Computed.MetadataDescriptionField, Somenamespace< /field >
    < field fieldName="ComputedCategory" storageType="YES" indexType="TOKENIZED" >Someproject.Indexes.Computed.CategoryField, Somenamespace< /field >
    ...
< /fields >

Tokenized versus Untokenized

An important setting in index field configuration is indexType.  Untokenized fields will be stored as one string in the Lucene index.  Tokenized fields will be broken up.

Facet Values Containing Spaces

One major gotcha that I encountered was facet values that contain spaces.  Because the facet fields are tokenized, the spaces in the facet values wrecked havoc with the facet results sets.  I found an excellent blog by Ryan Bailey, referencing a solution provided by Martina Welander.

Adding the computed facet fields to the fieldMap section solved the problem.


< fieldMap type="Sitecore.ContentSearch.FieldMap, Sitecore.ContentSearch" >
    < fieldNames hint="raw:AddFieldByFieldName" >
        ...
        < field fieldName="ComputedCategory" 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 >
        ...
    < /fieldNames >
< /fieldMap >

Dynamically Excluding Content

One of the requirements in this site is to be able to explicitly hide content from the site search based on an item field value.  I chose to create a custom crawler for this purpose.  The direct solution would be to directly filter on the results.  I chose to abstract this requirement into a crawler because of the complexity surrounding the search and faceting logic.  I did not want to complicate it further.

public class ExcludeItemCrawler : SitecoreItemCrawler
    {
        protected override bool IsExcludedFromIndex(SitecoreIndexableItem indexable, bool checkLocation = false)
        {
            bool isExcluded = base.IsExcludedFromIndex(indexable, checkLocation);

            if (isExcluded)
                return true;

            Item item = (Item)indexable;

            // If its a wildcard
            if (item.Name == "*")
                return true;

            // Several complex checks
            if (somecondition)
                return true;
	    ...
            return false;
        }
    }

 < locations hint="list:AddCrawler" >
     < crawler type="Somenamespace.ContentSearch.ExcludeItemCrawler, Somenamespace" >
         < Database >master < /Database >
         < Root >/sitecore/content < /Root >
     < /crawler >
 < /locations >
 < locations hint="list:AddCrawler" >
     < crawler type="Somenamespace.ContentSearch.ExcludeItemCrawler, Somenamespace" >
         < Database >master < /Database >
         < Root >/sitecore/media library < /Root >
     < /crawler >
 < /locations >

Ok, so now that we have the data that we want in the Lucene index, I’ll talk about how to query it in Part II.

Upgrading to Glass Mapper v4.3

Glass Mapper is hands down the best ORM available for Sitecore Development.  I have been using it for five years and love it.  It helps you map Sitecore templates and fields to .NET classes, objects and properties.

I recently decided to upgrade to v4.3 from v4.0.1.8.  Upgrading from v3 to v4 was a little bit hairy, but upgrading to v4.3 was pretty easy.

Update from Nuget

First, I loaded the new version of Glass Mapper from Nuget.  I opened the Package Manager Console and ran:

Install-Package Glass.Mapper.Sc -Version 4.3.4.197

Installing the Nuget package wipes out App_Start/GlassMapperScCustom.cs class, so I needed to re-add my custom data mappers. My GlassMapperScCustom class looked like this after I was done.

#region GlassMapperScCustom generated code
using Glass.Mapper.Configuration;
using Glass.Mapper.IoC;
using Glass.Mapper.Maps;
using Glass.Mapper.Sc.IoC;
using IDependencyResolver = Glass.Mapper.Sc.IoC.IDependencyResolver;
using Glass.Mapper.Configuration.Attributes;
using SomeProject.CustomDataHandler;

namespace SomeProject.App_Start
{
    public static  class GlassMapperScCustom
    {
        public static IDependencyResolver CreateResolver()
        {
           var config = new Glass.Mapper.Sc.Config();

	   var dependencyResolver = new DependencyResolver(config);
           // add any changes to the standard resolver here

           dependencyResolver.DataMapperFactory.Insert(0, () => new LinkListDataHandler());
           dependencyResolver.DataMapperFactory.Insert(0, () => new CustomInternalLinkDataHandler());

           return dependencyResolver;
        }

        public static IConfigurationLoader[] GlassLoaders()
        {
           var attributes = new AttributeConfigurationLoader("SomeProject");
           return new IConfigurationLoader[] { attributes };
        }
    
        public static void PostLoad()
        {
           var dbs = Sitecore.Configuration.Factory.GetDatabases();
           foreach (var db in dbs)
           {
               var provider = db.GetDataProviders().FirstOrDefault(x => x is GlassDataProvider) as GlassDataProvider;
               if (provider != null)
               {
                   using (new SecurityDisabler())
                   {
                       provider.Initialise(db);
                   }
               }
           }
        }
        public static void AddMaps(IConfigFactory< IGlassMap >  mapsConfigFactory)
        {
        }
    }
}
#endregion

Ambiguous Reference

I re-built my solution and ran into some compiler errors.

ambiguous reference error

Easy problem to resolve: a simple conflict between a Glass abstraction of the Sitecore Diagnostics log and the Sitecore Diagnostics log.  I considered removing the references to Sitecore.Diagnostics and using the Glass method, but the Glass.Mapper.Sc.Log class requires an instance to be used.  So, I simply globally changed my Log references in code to Sitecore.Diagnostics.Log.

Model Depth Check

The solution now compiled successfully.  When I tested the application, I encountered:

Server Error in ‘/’ Application.

Model too deep. Potential lazy loading loop. Type requested: SomeProject.Models.IGlassBase SomeProject.Models.IGlassBase
SomeProject.Models.IGlassBase
SomeProject.Models.IGlassBase
SomeProject.Models.IGlassBase
SomeProject.Models.IGlassBase
SomeProject.Models.IGlassBase
SomeProject.Models.Pages.HomePage
SomeProject.Models.Parts.DesktopMenu

After a quick Google search, I re-read the Glass Mapper 4.3 Release Notes.  The depth check mechanism added for the Cachable Changes was breaking the code.  This was another easy fix.  I refactored the CreateResolver method as per the release notes:

public static IDependencyResolver CreateResolver()
{
   var config = new Glass.Mapper.Sc.Config();

   // Needed to avoid Model too deep exception
   config.EnableLazyLoadingForCachableModels = true;
   
   var dependencyResolver = new DependencyResolver(config);
   // add any changes to the standard resolver here

   dependencyResolver.DataMapperFactory.Insert(0, () => new LinkListDataHandler());
   dependencyResolver.DataMapperFactory.Insert(0, () => new CustomInternalLinkDataHandler());

   var factory = dependencyResolver.ObjectConstructionFactory as AbstractConfigFactory < AbstractObjectConstructionTask >;
   factory.Remove< ModelDepthCheck >();

   return dependencyResolver;
}

Naughty GlassCast

The next issue I worked through was the depreciation of the GlassCast method. Mike Edwards, the “Godfather of Glass Mapper”, did an incredible job explaining why GlassCast can lead to performance problems. I had an embarrassing amount of GlassCast method calls sprinkled throughout the code base.

model = MiscContentFactory.GetRelatedContent (Sitecore.Context.Item.Parent.GlassCast< CustomModelClass >().Id.ToString(), parameters.NumberOfItems);

This was straight forward. I removed all references to GlassCast. I chose to replace GlassCast with use of the Cast method from the SitecoreService object.

ISitecoreService service = new SitecoreService(Sitecore.Context.Database);
model = MiscContentFactory.GetRelatedContent (service.Cast< CustomModelClass >(Sitecore.Context.Item.Parent).Id.ToString(), parameters.NumberOfItems);

Embedded Markup in RenderLink Contents Parameter

The next implementation issue was instances of RenderLink being used with embedded markup in the contents attribute.

@RenderLink(x => home.SomeLinkProperty, new System.Collections.Specialized.NameValueCollection() { { "class", "print-logo" } }, contents: "< span class=\"icon-openenvelop\" >< / span > < span class=\"print-logo\">" + home. SomeLinkProperty.Text + " < / span >")

I could not find a way to make this work.  I ended up converting every use of the method RenderLink to BeginRenderLink.

@using (BeginRenderLink(x => home.SomeLinkProperty, new System.Collections.Specialized.NameValueCollection() { { "class", "print-logo" } }, isEditable: true))
{
   < span class="icon-openenvelop" > < /span >< span class="print-logo" > @home. SomeLinkProperty.Text < / span >
}

I suppose I could have created a helper class to work around this to avoid all the rework, but I felt this was clearer long term solution.

Empty Model Values for View Renderings

The last issue that I encountered was View Renderings with blank Model class values.

blank view model value in Sitecore View Rendering

This caused the following error to appear on various pages:

The model item passed into the dictionary is of type ‘Sitecore.Mvc.Presentation.RenderingModel’, but this dictionary requires a model item of type ‘X’

Sloppy development practices left the Model values blank for some View Renderings, even though the View Renderings get bound to specific Model classes in the View.  I reviewed all of the View Renderings and set the Model values that were blank to the appropriate custom model class.

Sitecore View Rendering with valid model value

Conclusion

Upgrading to Glass Mapper v4.3 was fairly straightforward and well worth the effort.

Migrating Sitecore 7 Custom Indexes from Lucene to Solr

I recently found myself with the challenge of migrating a Sitecore 7 system from Lucene to Solr.  Several custom Lucene indexes were used for rendering critical UI elements.  Periodic issues with Lucene indexes becoming corrupted became problematic.  The site had outgrown Lucene, so a transition to Solr was needed, and sooner rather than later.

I’m a newbie to Solr, and found this a bit challenging, so I thought I’d write about it in case there is someone else out there on an older version of Sitecore, facing this same situation.

Which Version of Solr

I started by trying to determine which version of Solr to run.  I consulted the Sitecore Solr Compatibility Table.  I wanted the most current version of Solr that I could find that would result in the fewest compatibility issues with the old version of Sitecore. After reading Dan Solovay’s excellent blog post, I decided to use Solr v5.x, specifically v5.4.1-0.

How to Install Solr

I wanted to spin Solr up quickly and simply.  Bitnami seemed like an easy way to install the Apache Web Server and Solr.  Søren Engel wrote a great blog about using Bitnami to setup Solr with Sitecore 8.  I hunted around on the internet and managed to find a link to the older version of the installer.  Installing Apache Web Server and Solr using Bitnami is very easy.

Generating the Schema File

Next, I downloaded the Solr Support Package from the Sitecore SDN and installed it using the Sitecore Installation Wizard.  This tool attempts to modify the Solr Schema configuration so that you can use Solr with Sitecore.  Running the tool is easy, but it is out of date if you are using a newer version of Solr.

I found the Solr schema file in the “D:\Bitnami\solr-5.4.1-0\apache-solr\solr\configsets\basic_configs\conf” folder.  The file needs to be modified before running the Sitecore Solr Schema configuration tool.  Enclose all and elements with a tag.  Also enclose all elements with a tag.

Next, open the Sitecore Control Panel and run “Generate the Solr Schema.xml file” from the Indexing menu.  Open the output file, and make the following edits:

  1. pintshould be tint
  2. Add Treto the typessection.

This is the schema file that we will use for the Solr cores.

Creating Solr Cores

Similar to the issues that Dan Solovay described in his blog, I was unable to create a Solr core using Sitecore’s Search Scaling Guide.  I created a core as follows:

  1. Create a folder in the Solr folder (D:\Bitnami\solr-5.4.1-0\apache-solr\solr) and name it with the intended core name.
  2. Copied D:\Bitnami\solr-5.4.1-0\apache-solr\solr\configsets\basic_configs\conf folder to D:\Bitnami\solr-5.4.1-0\apache-solr\solr.
  3. Replace the schema.xml found in the conf folder with the Sitecore Schema file.
  4. Create a file name “core.properties” in the folder created in step 1 and add the following to the file:

name=testcore

config=solrconfig.xml

schema=schema.xml

dataDir=data

  1. Create a folder named “data” and a folder named “lib” in the folder created in Step 1.
  2. Stop and restart Solr using the Bitnami Apache Solr Stack Manager Tool found in the Windows Start menu.
  3. Check the Core Admin page in Solr to make sure that the core loaded successfully.

You will need to create a core for each custom index that your Sitecore implementation uses.

Changing Sitecore from Lucene to Solr

Now that Solr is running, lets switch Sitecore from Lucene to Solr.

In the Sitecore App_Config folder, disable the Lucene configuration files.  I renamed:

Sitecore.ContentSearch.Lucene.DefaultIndexConfiguration.config to Sitecore.ContentSearch.Lucene. DefaultIndexConfiguration.config.disable

Sitecore.ContentSearch.Lucene.Index.Master.config to Sitecore.ContentSearch.Lucene.Index.Master.config.disable

Sitecore.ContentSearch.Lucene.Index.Core.config to Sitecore.ContentSearch.Lucene.Index.Core.config.disable

Sitecore.ContentSearch.Lucene.Index.Web.config to Sitecore.ContentSearch.Lucene.Index.Web.config.disable

Add the Sitecore.ContetnSearch.Solr.Indexes.config from the Solr Support package.

Defining Solr Service Base Address

In the Sitecore.ContentSearch.Solr.Indexes.config, change the Solr address to your Apache Solr stack.

servicebase

I needed to move this setting to the web.config file so that it was accessible from the web.config transform I run in the deployment process. I also needed to bump up the Solr results limit in this file:

solrmax

The default is 500, which would not work for this site.

Dlls

I elected to use Castle Windsor, since I am already using Glass Mapper.  Make sure that the following dlls are in the bin folder:

SolrDlls

You may have to use nuget to install Castle Windsor.  I didn’t need to do this since I already had Glass Mapper set up.

Code Changes

In Global.asax, change:

<%@Application Language='C#' Inherits="Sitecore.Web.Application" %>

To

<%@Application Language='C#' Inherits="Sitecore.ContentSearch.SolrProvider.CastleWindsorIntegration.WindsorApplication" %>

Custom Index Configurations

I created a custom configuration for each index.  I filtered unwanted content out by using Include Template filtering.

includetemplate

I also defined the fields that I needed in the index in the fieldNames and IncludeField sections.

fieldNames

includeField

Custom Computed Fields go in computedfield.

computedFields

Custom Index Definition

I added the custom index definitions that I needed to the web.config file in configuration/sitecore/contentSearch/configuration. I add my custom indexes in the web.config file because I alter the web.config file based on a web.config transform that runs in the deployment process.  Some indexes get removed depending on the server.

indexdefinition

Conclusion

That’s it.  I really had very few code changes to make and they were largely due to some irregularity in the content in the existing Lucene indexes.  I was surprised at how easy the migration was from this perspective, considering the large number of custom indexes that were defined.  All of my computed fields and index references worked with no changes needed.