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.

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.