Avoid a FOUCed content delivery experience using CSS and JavaScript

A Flash of Unstyled Content (FOUC) problem occurs when unstyled content is displayed in its raw form during page load, then later laid out as desired. This can be a problem in a Sitecore site as in any other; recently we encountered this while using the jQuery Exposure plugin.

To avoid this, try placing the following, or a functional equivalent, in the <head> section of your layout, after the script tag for jQuery itself:

<style>
.no-fouc {
    visibility : hidden ;
}
</style>

<script>
    $(function () { $( ".no-fouc" ).removeClass("no-fouc"); });
</script>

Then simply apply the no-fouc class to an appropriate container of the elements causing the problem.

Easily construct site-resolving URLs in Sitecore using extension methods

The LinkManager class is familiar to anyone who has had to construct URLs in Sitecore. It’s useful, especially with the site resolution feature, designed to help one construct cross-domain links. However, it’s important to remember to set the targetHostName attribute in the site configuration for best results.

As one would expect, given the high degree of extensibility of the Sitecore platform, one can extend and change the default link provider mechanism used to generate links. However, in a large number of cases, writing a custom class and configuring it properly, or even constructing a UrlOptions object explicitly and using it in calls to LinkManager, can be avoided by using a few extension methods. Just as the LinkManager.GetItemUrl() method was designed to make link generation easier, a single-shot method for constructing URLs, right from any item, would make things easier in many programming scenarios.

Below is sample code for such an approach. Features include the ability to programmatically set URL options as the default for a site or container, and a method to call from an Item instance to generate a URL, with the option to override the URL option defaults. Site resolution is performed for any content item in a site configured in Web.config with the targetHostName property, even if in a different site from the context item. If desired, one can tweak this approach further, such as by constructing the default URL options used here from configuration settings dealing with links and site resolution.

There’s nothing wrong with the LinkManager implementation, but a bit of sugar such as this would make it easier to use the framework. It wouldn’t prevent switching to a different link provider implementation, either, since the static extension methods simply front-end the built-in API.

public static class SitecoreDataItemExtensions
{
   static SitecoreDataItemExtensions()
   {
      defaultUrlOptions = new Sitecore.Links.UrlOptions();
      defaultUrlOptions.AddAspxExtension = false;
      defaultUrlOptions.AlwaysIncludeServerUrl = true;
      defaultUrlOptions.LanguageEmbedding = Sitecore.Links.LanguageEmbedding.Never;
      defaultUrlOptions.LowercaseUrls = true;
      defaultUrlOptions.ShortenUrls = true;
      defaultUrlOptions.SiteResolving = true;
      defaultUrlOptions.UseDisplayName = false;   
   }

   private static System.Collections.Concurrent.ConcurrentDictionary<string, Sitecore.Links.UrlOptions> siteUrlOptionsMap = new System.Collections.Concurrent.ConcurrentDictionary<string,Sitecore.Links.UrlOptions>();
   private static Sitecore.Links.UrlOptions defaultUrlOptions;

   /// <summary>
   /// Constructs and returns a clone of the default URL options for the specified site
   /// </summary>
   /// <param name="siteContext">The site for which to get the default URL options</param>
   /// <returns>A clone of the URL options, using the container-wide default as a fallback, with the specified site context applied</returns>
   private static Sitecore.Links.UrlOptions GetDefaultUrlOptions(Sitecore.Sites.SiteContext siteContext)
   {
      Sitecore.Links.UrlOptions urlOptions = defaultUrlOptions;
      try { siteUrlOptionsMap.TryGetValue(siteContext.Name, out urlOptions); } catch {}
      if (urlOptions == null)
         urlOptions = defaultUrlOptions;

      urlOptions = (Sitecore.Links.UrlOptions)urlOptions.Clone();
      urlOptions.Site = siteContext;
      return urlOptions;
   }

   /// <summary>
   /// Sets a clone of these UrlOptions as the default to use for all sites in this container
   /// </summary>
   public static void SetAsDefault(this Sitecore.Links.UrlOptions urlOptions)
   {
      if (urlOptions == null) return;

      Sitecore.Links.UrlOptions clone = (Sitecore.Links.UrlOptions)urlOptions.Clone();
      clone.Site = null;
      defaultUrlOptions = clone;
   }

   /// <summary>
   /// Sets a clone of this set of URL options as the default for the specified site
   /// </summary>
   /// <param name="siteContext">The optional site for which to set these UrlOptions as the default, overriding any which are set on the UrlOptions directly</param>
   public static void SetAsSiteDefault(this Sitecore.Links.UrlOptions urlOptions, Sitecore.Sites.SiteContext siteContext = null)
   {
      if (urlOptions == null) return;
      else if (siteContext == null)
      {
         urlOptions.SetAsDefault();
         return;
      }

      urlOptions = (Sitecore.Links.UrlOptions)urlOptions.Clone();
      urlOptions.Site = siteContext;
      urlOptions.SiteResolving = true;
      
      siteUrlOptionsMap.AddOrUpdate(siteContext.Name, urlOptions, (k, v) => urlOptions);
   }

   /// <summary>
   /// Sets a clone of this set of URL options as the default for the specified site
   /// </summary>
   /// <param name="siteName">The optional name of the site for which to set these UrlOptions as the default, overriding any which are set on the UrlOptions directly</param>
   public static void SetAsSiteDefault(this Sitecore.Links.UrlOptions urlOptions, string siteName = null)
   {
      if (urlOptions == null) return;

      Sitecore.Sites.SiteContext siteContext = null;
      if (!string.IsNullOrWhiteSpace(siteName))
      {
         try
         {
            siteContext = Sitecore.Sites.SiteContextFactory.GetSiteContext(siteName);
         } 
         catch (Exception e) {
            throw new InvalidOperationException("Could not find site '" + siteName + "' (Check Web.config <sites>); " + e.Message);
         }
         if (siteContext == null) 
            throw new InvalidOperationException("Could not find site '" + siteName + "' (Check Web.config <sites>)");
      }
      
      if (siteContext == null)
         siteContext = urlOptions.Site;

      urlOptions.SetAsSiteDefault(siteContext);
   }

   private static List<Sitecore.Sites.SiteContext> siteContexts = null;

   /// <summary>
   /// Gets a list of site contexts for this container
   /// </summary>
   private static List<Sitecore.Sites.SiteContext> SiteContexts
   {
      get 
      {
         if (siteContexts == null)
         {
            List<Sitecore.Sites.SiteContext> siteContextList = new List<SC.Sites.SiteContext>();

            Sitecore.Sites.SiteContext sc;
            foreach (string siteName in Sitecore.Sites.SiteContextFactory.GetSiteNames())
            {
               sc = Sitecore.Sites.SiteContextFactory.GetSiteContext(siteName);
               if (sc != null) siteContextList.Add(sc);
            }

            siteContexts = siteContextList;
         }

         return siteContexts;
      }
   }

   /// <summary>
   /// Gets the site context, if any, for this item
   /// </summary>
   public static Sitecore.Sites.SiteContext GetSiteContext(this Sitecore.Data.Items.Item item)
   {
      if (item == null) return null;
      string itemPath = item.Paths.FullPath.ToLower();

      foreach (Sitecore.Sites.SiteContext siteContext in SiteContexts)
         if (!string.IsNullOrWhiteSpace(siteContext.RootPath) &&
            siteContext.RootPath.StartsWith("/sitecore/content/") &&
            (siteContext.RootPath.Length > 18) &&
            itemPath.StartsWith(siteContext.RootPath.ToLower()))
            return siteContext;

      return null;
   }

   /// <summary>
   /// Gets a URL for this item; if no optional URL options are supplied, uses site-resolving default URL options
   /// </summary>
   public static string GetUrl(this Sitecore.Data.Items.Item item, Sitecore.Links.UrlOptions urlOptions = null)
   {
      if (item == null) throw new ArgumentNullException();
      Sitecore.Sites.SiteContext siteContext = item.GetSiteContext();

      if (urlOptions == null)
      {
         if (siteContext != null)
            urlOptions = GetDefaultUrlOptions(siteContext);
      }
      else if (siteContext != null)
      {
         urlOptions.Site = siteContext;
      }

      if (urlOptions == null)
         return Sitecore.Links.LinkManager.GetItemUrl(item);
      else
         return Sitecore.Links.LinkManager.GetItemUrl(item, urlOptions);
   }

}

Use ASP.NET output caching safely with post-back

The venerable ASP.NET output caching mechanism is useful for caching HTML fragments generated by controls. This can have a massive impact on performance and scalability of sites, but the implementation is not without drawbacks. One common problem encountered with web forms is how to use output caching, but disable it for post-back or similar scenarios. This can be important, for example, in presenting paged search results, where it can be advantageous to cache the first page of results but users may less frequently go to the second page.

In order to do this, override the GetVaryByCustomString method in Global.asax as follows:

  /// <summary>
  /// Added to support control output caching, varying by URL. 
  /// </summary>
  public override string GetVaryByCustomString(HttpContext context, string custom)
  {
	  switch (custom.ToUpper())
	  {
		  case "RAWURL":
			  {

				  if (context.Request.RequestType.Equals("POST"))
				  {
					  context.Response.Cache.SetNoServerCaching();

					  return "POST " + DateTime.Now.Ticks + " " + context.Request.RawUrl;
				  }
				  else 
					  return context.Request.RawUrl;
			  }
		  default:
			  return "";
	  }
  }

Then, on any control for which you wish to enable output caching but only if the page load is not a post-back, add the following directive:

<%@outputcache duration=”3600″ varybyparam=”none” varybycustom=”RAWURL” %>

Note that the ‘duration’ value is in seconds, and set it accordingly. Obviously the ‘varybycustom’ value can be set to any value desired, as long as it is trapped appropriately in GetVaryByCustomString().

One can easily combine this approach with one sensitive to different cookies as well (one example). One could similarly key off of page-level variables stored in view state, application state variables, etc.; the name of the variable by which to vary may for example be stuffed, with an appropriate prefix, into ‘varybycustom’ in the directive. Strategies like these can be used to achieve a range of different effects, for instance to use output caching safely with paging.

Tips for dealing with missed breakpoints in Visual Studio

I’ve been doing a great deal of work with Visual Studio 2012 and Sitecore lately. Our largest solution contains nearly twenty projects with five web projects, and this one in particular suffers from “lost” or missed breakpoints during debugging. Seeing the empty red circle can be very frustrating on a tight deadline. Following are some tips that may help, with a nod to Stack Overflow.

  • Make sure that your project(s) are in debug mode. You can easily check this by right-clicking the solution and selecting Properties – Configuration Properties – Configuration.
  • Turn off optimizations in project properties, in the “Build” section.
  • Check project references, to make sure you are getting the latest built versions of assemblies. For intra-solution references, prefer to make references to other projects’ output instead of browsing to files.
  • Make sure that you are debugging all projects containing your breakpoints. Everyone is familiar with the “Set as Startup Project” option available when right-clicking a project within Visual Studio, but you can also set multiple projects to be debugged.
  • In Options – Debugging – General, ensure that "Enable Just My Code" is deselected.
  • While debugging, choose menu item Debug – Windows – Modules. For each module/assembly containing a skipped breakpoint which is listed as "Skipped loading symbols", right-click and select "Load symbols".
  • Get a clean build and set of debug symbols: Manually kill w3wp.exe or other process hosting your code, delete /obj/ folders, delete build assemblies and .pdb files, add or delete a meaningless character from a source file in each debugged project, use “Build – Clean Solution” and/or “Build – Clean [project name]”, and then rebuild.
  • Check “Debug – Windows – Breakpoints” to make sure that your breakpoints are recognized (they should be listed, checked, and filled with red).
  • Restart Visual Studio.
  • Make sure you are attaching to the correct process. For web projects I generally select project properties, select the “Web” side tab, and select “Use Local IIS Web server”, then attach to w3wp.exe .
  • If all else fails, insert System.Diagnostics.Debugger.Break() in your code, do a clean build and reattach. This sometimes seems to reset the debugger when it gets hinky, to the point that it will start recognizing breakpoints added through Visual Studio again. You can then delete the line of code, rebuild and reattach.

Seamlessly switch Lucene indexes to avoid search downtime during re-indexing

(For those of you who don’t already know, John West is the Sitecore person. He wrote the book on Sitecore development, and his blog in particular is a treasure trove of Sitecore knowledge. He’s also an all-around nice guy, which I suppose fits his role as a technological evangelist; he’s always lending a hand to someone.)

During our current Sitecore upgrade, I found this gem: using the Lucene subdirectory-switching feature, via Sitecore.ContentSearch.LuceneProvider.SwitchOnRebuildLuceneIndex , allows for zero downtime when re-indexing in a production environment. Note that due to the use of two indexes, it may be necessary to rebuild twice when implementing indexing changes.

Some brief musings on search

We’re currently in the middle of upgrading a major codebase from Sitecore 6.6 to Sitecore 7.2 (and from thence to 7.5 or directly to 8.x). This has prompted a lot of soul-searching–how did we get here? Are we alone in the universe? And after a cup of coffee, some search-related thoughts have begun to emerge as well…

Solr is an exciting prospect, for sure, and the ContentSearch namespace is definitely much improved over what it’s replaced. Any pluggable architecture for any data-layer component always gives me the warm fuzzies. It’s actually kind of hot, in a strictly nerdy, programmatic sense.

Using PredicateBuilder to put together detailed queries can be a breeze. Still, the API is a bit clunky (I am apparently not alone in feeling so) and documentation seems to be a bit scattered. It seems that with a more careful eye to design, or maybe adding a bit of syntactic sugar somehow, the need for ugliness like an anchoring true-or-false condition could have been avoided, and the resulting programming style more natural.

Another persistent gripe of mine is the lack of some unification for Sitecore Query and Lucene (and now Solr) searches. The beauty of Sitecore Query, of course, is the beauty of XPath–although it may be a somewhat twisted, hobbled version, the orc to XPath-proper’s elf–and that is a declarative syntax. That’s why it’s used in tons of places within Sitecore, for data sources and more, as well as in third-party libraries like the wonderful Glass Mapper, which we use. You can’t beat the simplicity of referring, in your complex model, to related objects using an easily composed attribute path.

The fact is, in at least some situations where Lucene/Solr are valid choices for performance reasons, the actual search could be shoehorned into an XPath-like syntax too. Food for thought? I’m currently investigating something based on this… more later.

Quirkiness accessing the site root item from a descendant using Sitecore Query

Sitecore Query, with its XPath-like query syntax, has its share of quirks. It doesn’t always quite return the expected results, and one can’t use all functions available in later versions of XPath, either (though it does have some very useful Sitecore-specific functions). One can tackle these issues by extending Sitecore Query to include desired functionality. However, depending on the context this may not work well. Swapping out the built-in expression evaluation would be non-trivial (though useful, for example to merge Sitecore Query functionality with non-Sitecore search while retaining a declarative programming style, integrating with an ORM such as the Glass Mapper, or making non-Sitecore-Query results available everywhere in Sitecore itself that Sitecore Query is used, a topic for another exploration).

Everyone who’s used Sitecore extensively has had to access the site root item. This is not tough programmatically using the Sitecore API, and often done using extension methods. But when it’s helpful to use Sitecore Query, especially where the context item is at an undetermined depth in the content tree, one can run up against limitations of Sitecore Query.

I first discovered this on a project using Sitecore 7.2. The obvious approach is to use the ancestor axis in the query, but using index-based queries turned out to be funky; queries with an index of 1 would return the top three items (/sitecore, /sitecore/content, and the relevant child item of /sitecore/content). I found that using the following query would return the item at the specified level plus two, in this case actually returning the site-root item I was after:

./ancestor::*[ancestor::*[@@key='content']]/.[1]

Out of curiosity I determined that the following would also work, with varying degrees of hackiness:

./ancestor::*[@@key='home']/ancestor::*[@@key != 'sitecore' and @@key != 'content']
./ancestor::*[position() = 1 and @@key != 'sitecore' and @@key != 'content']
./ancestor::*[ancestor::*[@@key='content']]/.[1]
./ancestor::*[ancestor::*[parent::content]]/.[1]

These are written for readability; one could use @@templateid instead of @@key with the template ID of the /sitecore/content item as well, or simply restrict to the template of the site home/root item itself, where that template is guaranteed to be unique in the hierarchy as it often is. The latter is what I wound up doing and it worked fine, but I came away with an enhanced respect for the sheer individuality of Sitecore Query.

Temporarily disable Sitecore client notifications to suppress unwanted new-item redirects

The default behavior, when a user kicks off any action in the Sitecore client which adds a new item, is that the client redirects to the newly created item. In some (many) cases this may be desirable, but in others it is an unwanted side effect.

In order to suppress this behavior, two approaches may be helpful. One sometimes recommended is to suppress all events while creating the new item, but this should be done with caution depending on the environment. To do this enclose the item-creation code in a using statement like this one:

using (new Sitecore.Data.Events.EventDisabler()
{
// item creation code here...
}

Even better, and an approach which should be sufficient by itself, is to suppress client notifications during the item creation, like so:

Sitecore.Client.Site.Notifications.Disabled = true;
// item creation code here...
Sitecore.Client.Site.Notifications.Disabled = false;

To be safe, be sure to re-enable client notifications in the finally of a try-catch block. Also, depending on the Sitecore version, bucketable items may need a special tweak to suppress notifications as well.

Resetting the admin password in Sitecore, and some security considerations

As of Sitecore 7, one can still reset the admin password directly in the core database by executing the following statement:

UPDATE [aspnet_Membership] SET Password='qOvF8m8F2IcWMvfOBjJYHmfLABc='   
WHERE UserId IN (
     SELECT UserId FROM [aspnet_Users] WHERE UserName = 'sitecore\Admin'
)

This resets the admin password to the default value, “b”. Obviously a reset is in order shortly afterward, and this highlights the need for good security at the database level, but this tip can be helpful if the password is reset to a value that’s somehow lost.

Here’s the kicker: resetting the admin password may be necessary even in environments where the Sitecore client is disabled.  Perhaps the client-disabled environment was set up with the default password, or it simply needs to be changed due to security requirements. The password may still actually be used in a client-disabled environment as well, if the admin pages are not also disabled.

If that is the case, one easy approach may be to:

  1. Reset the admin password in a non-client-disabled environment;
  2. Get the stored, hashed password by running the following query in the core database of the non-client-disabled environment; and
    SELECT Password FROM [aspnet_Membership]   
    WHERE UserId IN (
         SELECT UserId FROM [aspnet_Users] WHERE UserName = 'sitecore\Admin'
    )
  3. Copy the password to the client-disabled environment, by running UPDATE statement above with the changed password in the core database of the client-disabled environment.