Wednesday, December 6, 2017

Sitecore's Jobs.aspx Admin Page for Sitecore v6.6 to v7.1


Up until the release of Sitecore 7.2 Update-6 Sitecore was missing an out-of-the-box Jobs Viewer tool - which resulted in community developers rolling their own and sharing to the rest of us.

Here are links to some of the community-driven implementations:

https://www.geekhive.com/buzz/post/2015/04/sitecore-job-viewer/

https://briancaos.wordpress.com/2014/11/11/sitecore-job-viewer-see-what-sitecore-is-doing-behind-your-back/ 

https://marketplace.sitecore.net/en/Modules/V/View_Sitecore_Jobs.aspx 

https://sitecoreblog.marklowe.ch/2014/06/view-running-sitecore-background-jobs/ 

One feature that separated Sitecore's Jobs.aspx page from the community-driven implementations was its ability to auto-refresh and essentially view jobs running in real-time without the need for a refresh button. 

It also looked very nice! :)

I wanted to use THIS Jobs.aspx page on older versions of Sitecore I still maintained, so I started decompiling the Sitecore.Kernel, SitecoreClient, and Sitecore.ExperienceContentManagement.Administration binaries from v8.1 in an attempt to consolidate all the code into one Jobs.aspx page - no additional dll required.

Here's the result:

  <%@ Page language="c#" EnableEventValidation="false" AutoEventWireup="true" Inherits="Sitecore.sitecore.admin.AdminPage" %>   
  <script runat="server">   
   protected override void OnInit(EventArgs e)   
   {   
   base.CheckSecurity(true); //Required!   
   base.OnInit(e);   
   }   
  void Page_Load(object sender, System.EventArgs e)   
  {   
       Sitecore.Jobs.JobManager.GetJobs();   
     StringBuilder stringBuilder = new StringBuilder();   
     Type type = typeof(Sitecore.Jobs.JobManager);   
     this.ShowRefreshStatus(stringBuilder);   
     System.Reflection.FieldInfo field = type.GetField("_runningJobs", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);   
     if (field != null)   
     {   
      this.ShowJobs(stringBuilder, "Running jobs", ((Sitecore.Collections.SafeDictionary<Sitecore.Handle, Sitecore.Jobs.Job>)field.GetValue(null)).Values.ToArray<Sitecore.Jobs.Job>());   
     }   
     System.Reflection.FieldInfo fieldInfo = type.GetField("_queuedJobs", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);   
     if (fieldInfo != null)   
     {   
      this.ShowJobs(stringBuilder, "Queued jobs", ((Sitecore.Collections.JobCollection)fieldInfo.GetValue(null)).ToArray<Sitecore.Jobs.Job>());   
     }   
     System.Reflection.FieldInfo field1 = type.GetField("_finishedJobs", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);   
     if (field1 != null)   
     {   
      this.ShowJobs(stringBuilder, "Finished jobs", ((Sitecore.Collections.JobCollection)field1.GetValue(null)).Reverse<Sitecore.Jobs.Job>().ToArray<Sitecore.Jobs.Job>());   
     }   
     this.lt.Text = stringBuilder.ToString();   
  }   
  protected virtual void ShowJobs(StringBuilder stringBuilder, string name, ICollection<Sitecore.Jobs.Job> enumerable)   
    {   
     stringBuilder.AppendLine(string.Concat("<h1>", name, ":</h1><br />"));   
     if (enumerable.Count <= 0)   
     {   
      stringBuilder.AppendLine("<b>No jobs</b><br />");   
     }   
     else   
     {   
      stringBuilder.AppendLine("<table class='jobs-table'>");   
      stringBuilder.AppendLine("<thead><tr><td class='counter'>No</td><td class='add-time'>Added</td><td class='title'>Title</td><td class='progress'>Progress</td><td class='priority'>Priority</td></tr></thead>");   
      int num = 1;   
      foreach (Sitecore.Jobs.Job job in enumerable)   
      {   
       long total = job.Status.Total;   
       TimeSpan localTime = TimeSpan.Zero;   
       string str = (localTime.Hours == 0 ? string.Empty : string.Concat(localTime.Hours, "h "));   
       string str1 = (localTime.Minutes == 0 ? string.Empty : string.Concat(localTime.Minutes, "m "));   
       stringBuilder.AppendLine("<tr>");   
       stringBuilder.AppendLine(string.Concat("<td class='counter'>", num, "</td>"));   
       object[] longTimeString = new object[] { "<td class='add-time'>", null, null, null, null, null, null };   
       DateTime dateTime = job.QueueTime.ToLocalTime();   
       longTimeString[1] = dateTime.ToLongTimeString();   
       longTimeString[2] = " (";   
       longTimeString[3] = str;   
       longTimeString[4] = str1;   
       longTimeString[5] = localTime.Seconds;   
       longTimeString[6] = "s ago)</td>";   
       stringBuilder.AppendLine(string.Concat(longTimeString));   
       stringBuilder.AppendLine(string.Concat("<td class='title'>", job.Name, "</td>"));   
       StringBuilder stringBuilder1 = stringBuilder;   
       object[] processed = new object[] { "<td class='progress'>", job.Status.Processed   
              , null, null };   
       processed[2] = (total > (long)0 ? string.Concat(" of ", total) : string.Empty);   
       processed[3] = "</td>";   
       stringBuilder1.AppendLine(string.Concat(processed));   
       stringBuilder.AppendLine(string.Concat("<td class='priority'>", job.Options.Priority, "</td>"));   
       stringBuilder.AppendLine("</tr>");   
       num++;   
      }   
      stringBuilder.AppendLine("</table>");   
     }   
     stringBuilder.AppendLine("<br /><hr />");   
    }   
    protected virtual void ShowRefreshStatus(StringBuilder stringBuilder)   
    {   
     int num;   
     string str;   
     string item = base.Request.QueryString["refresh"];   
     int.TryParse(item, out num);   
     object[] objArray = new object[1];   
     DateTime now = DateTime.Now;   
     objArray[0] = now.ToString(System.Globalization.CultureInfo.InvariantCulture);   
     stringBuilder.Append(Sitecore.StringExtensions.StringExtensions.FormatWith("Last updated: {0}. ", objArray));   
     int[] numArray = new int[] { 1, 2, 5, 10, 20, 30, 60 };   
     stringBuilder.Append(string.Format("Refresh each <a href='jobs.aspx' class='refresh-link {0}'>No Refresh</a>", (num == 0 ? "refresh-selected" : string.Empty)));   
     int[] numArray1 = numArray;   
     for (int i = 0; i < (int)numArray1.Length; i++)   
     {   
      int num1 = numArray1[i];   
      str = (num == num1 ? "refresh-selected" : string.Empty);   
      string str1 = string.Format(", <a href='jobs.aspx?refresh={0}' class='refresh-link {1}'>{0} sec</a>", num1, str);   
      stringBuilder.Append(str1);   
     }   
     stringBuilder.Append("<br /><br />");   
    }   
  </script>    
  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >   
  <html xmlns="http://www.w3.org/1999/xhtml">   
   <head runat="server">   
    <title>Jobs Viewer</title>   
    <link rel="Stylesheet" type="text/css" href="/sitecore/shell/themes/standard/default/WebFramework.css" />   
    <link rel="Stylesheet" type="text/css" href="./default.css" />   
    <style type="text/css">   
     .jobs-table {   
      border: solid 1px grey;    
      border-spacing: 2px;   
      border-collapse: separate;   
      width: 100%;   
     }   
     .jobs-table td {   
      padding: 2px;   
     }   
     .jobs-table thead {   
      font-weight: bold;   
     }   
     .jobs-table .counter {   
      width: 25px;   
      text-align: right;   
     }     
     .jobs-table .add-time {   
      width: 150px;   
     }     
     .jobs-table .title {   
      word-break: break-all;   
     }    
     .jobs-table .progress {   
      width: 50px;   
      text-align: center;   
     }    
     .jobs-table .priority {   
      width: 80px;   
     }        
    </style>   
   </head>   
   <body>   
    <form id="Form1" runat="server" class="wf-container">   
     <div class="wf-content">   
      <h1>   
       <a href="/sitecore/admin/">Administration Tools</a> - Jobs Viewer   
      </h1>   
      <br />   
      <asp:Literal runat="server" ID="lt"></asp:Literal>   
      <script type="text/javascript">   
       function getQueryString() {   
        var result = {}, queryString = location.search.substring(1), re = /([^&=]+)=([^&]*)/g, m;   
        while (m = re.exec(queryString)) {   
         result[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);   
        }   
        return result;   
       }   
       var str = getQueryString()["refresh"];   
       if (str != undefined) {   
        c = parseInt(str) * 1000;   
        setTimeout("document.location.href = document.location.href;", c);   
       }   
      </script>   
     </div>   
    </form>   
   </body>   
  </html>   

Here is the Sitecore's Jobs Viewer admin tool working on a clean Sitecore 7.0 site:


Simply copy the code above into a new Jobs.aspx file and drop it into your site's /sitecore/admin folder.

I've confirmed this working on versions as far back as Sitecore versions 6.6.

Enjoy!

Friday, November 17, 2017

Quick Tip: Count Child Items of Sitecore Tree using Google Chrome Dev Console

There have been several requests among different clients our practice works with to provide a simple count of items under a part of a Sitecore tree.

While writing a Sitecore query and running it against the XPath builder is completely feasible, there is a quicker way if you're just trying to get a child count.

Open up the Sitecore Content Editor, activate the node you want to get the number of children under and expand it.

Open up your Google Chrome Dev Console and run the following line of Javascript:

 document.getElementsByClassName('scContentTreeNodeActive')[0].nextSibling.childNodes.length  

This one-liner finds the active Content Tree node and discovers the number of child items living under it.



Note: This doesn't account for all descendants and only seems to work with Google Chrome, however, I've found this useful in a few situations now across any Sitecore version.

Enjoy!


Tuesday, September 5, 2017

Paid Google Site Search is Going Away. What are my options in Sitecore?



I've seen this email a few times now - and as unfortunate as Google's decision is on removing a product that has powered hundreds of thousands of website's site search - this is real.

What does this mean for Sitecore site searches using GSS?

Depending on the original implementation, it could mean the search page stops working altogether - which could occur if your search results have been served via XML.  The XML results will no longer be served, and therefore your site won't have data to build the results page.

Alternatively, if the GSS implementation was a straight script and tag injection, the search page will still work, but you'll be met with 3-5 advertisements on the top results of your search results.

None of these options are ideal, of course.

In addition to one of those actions occurring on your site, you'll also lose any email support from Google, and be left to the mercy of capped query limits.

What are our options?

Luckily, there are a few.  Here are just some you can explore:

Keep what you can (if you can).

If you've confirmed that Google will still serve results and are okay with advertisements (potentially from competitors) slipped onto your website, then do nothing.  Just remember, if your monthly quota is reached, the search pages will stop showing results.

Leverage Sitecore's OOB Lucene Indexes

Sitecore comes with a fairly powerful search engine library and API, making it pretty straight forward to recreate what Google has been serving all built into your solution.  An experienced Sitecore developer should be able to whip up a straight site search based on a template restricted Lucene index in a reasonable timeline.

Odds are that all styles used in the GSS implementation can be reused for the Sitecore Search API implementation.  If you require a more complex solution such as facets / filtering / sorting, etc - this will obviously require more development time - but can be done in phases if the goal is to change over from GSA as soon as possible.  


If you're in the cloud, try Azure Search service

If you use Sitecore Azure PaaS and on Sitecore Experience Platform 8.2 Update-1 or above, you can use Azure Search are your primary search engine. It works similarly to the OOB Lucene implementation (with some exceptions) but is rather a service that lives in the Azure cloud and connected to your solution via a connection string.

Configuration is handled similarly to a Solr implementation and offers "extreme scalability, simplicity, and stability".  The Azure Search service also guarantees 99.95% uptime as a part of the Microsoft Azure service level agreement and can be easily scaled up or down on an as-needed basis.  
There are certainly some limitations with Azure search as of now, but it is worth exploring when choosing a replacement for GSA. 

More information:

Go with Coveo

Coveo offers an extremely powerful search platform that works on top of Sitecore and comes with all the bells and whistles of what you'd expect from a reputable search company.

Coveo features the ability to provide relevant search results, self-learning/self-tuning search, a secure and scalable cloud service, responsive design, and full integrated Sitecore components.   Coveo OOB allows Content Authors to build rich search pages with Sitecore Page Editor, tune search and promote content using Sitecore Rules Engine, and manage search indexes and crawl schedules via Sitecore Index Manager all without additional development resources.

With Coveo Cloud, you don't need to worry about any additional on premise hardware - but you should still be conscious of the additional license costs associated with this option.

If your goal is to take this opportunity to swap in a more elegant search solution - Coveo can help you upgrade the search experience, as opposed to simply replacing it.  

More information:

Searching with Solr

Solr has been a viable option for Sitecore search for years.  Comparable to OOB Lucene, Solr is an enterprise-level Lucene-based search platform making it easier to scale for larger implementation vs the OOB Lucene engine.    Ensuring compatibility for the version of Solr you should be using is critical in your decision here.

There are plenty of Sitecore provided resources and community driven blogs out there to help you get started.

More information:
Sitecore Docs - Walkthrough: Setting up Solr


Other Plug-and-Play Third Party Search Libraries

If development resources are strapped, you can explore an alternative like Bing Search or Cludo which would likely cut on internal development time, but also comes at an additional monthly cost to keep running.   I personally have little experience working with either of these, but both may be feasible in a pinch. 


Do you know of other options not mentioned here?
Feel free to comment below and describe your experience!

Monday, August 28, 2017

Sitecore Text Searcher - The Ultimate SQL Script

"Where can I edit this text in Sitecore?"
"Can you find out where this content can be changed in Sitecore? 
"I tried changing this content in Sitecore here, but it's not reflecting on the front-end after I publish." 

I hear that a lot.  

When maintaining multiple Sitecore solutions - knowing every template, rendering, and how the data ties in to what you see on the front-end - is nearly impossible.   Prior to Helix, developers were free to develop using various patterns. Some are built in MVC, others are WebForms.  Some components rely on datasource items, others rely on data from the items template. Regardless of architecture, we need to be able to help Content Authors change content if they can't find what they're looking to change.

There are typically a few options to obtain the information for the Content Author's request:

     - If you have access to the solution - inspect the page elements, grab a common class or id, do a Entire Solution search, find some files and follow the code to determine how it populates the component.

     - Start with the page item in Sitecore and determine whether the content is located on the item's template, or in within a data source item.

Those options are both fine - and eventually effective - but there is a faster way - IF YOU HAVE ACCESS TO SQL (which is normally the case if you have a recent DB locally or access to the DB server).

Simply start a new query in SQL MS on the Sitecore DB you want to search in (Web or Master),  modify the @SEARCHTERM variable (include the percent symbols eg. '%content goes here%'), and execute this handy script:



DECLARE @SITECOREROOTID NVARCHAR(MAX), @FULLPATHSEARCH NVARCHAR(MAX), @SEARCHTERM NVARCHAR(MAX)

SET @SITECOREROOTID = '11111111-1111-1111-1111-111111111111';
SET @FULLPATHSEARCH = 'sitecore/content/%';
SET @SEARCHTERM = '%search term%';

WITH FullPathItems (ID, NAME, ITEMPATH, TEMPLATEID)
    
AS (SELECT ID,
                
NAME,
                
CAST(NAME AS NVARCHAR(MAX)) AS itempath,
                
TEMPLATEID
        
FROM   [dbo].ITEMS
        
WHERE  ID = @SITECOREROOTID
        
UNION ALL
        
SELECT i.ID,
                
i.NAME,
                
CAST(( ITEMPATH + '/' + i.NAME ) AS NVARCHAR(MAX)) AS itempath,
                
i.TEMPLATEID
        
FROM   [dbo].ITEMS i
                
INNER JOIN FullPathItems a
                        
ON i.PARENTID = a.ID)
                        
SELECT fieldsX.ITEMID,
      
fpi.NAME AS ITEMNAME,
      
fieldsX.VALUE,
      
fpi.TEMPLATEID AS TEMPLATEID,
      
templatesX.NAME AS TEMPLATENAME,
      
ITEMPATH,
      
fieldsX.FIELDID,
      
itemsX.NAME AS FIELDNAME,
      
fieldsX.LANGUAGE  AS ITEMLANGUAGE 

       FROM   [dbo].[FIELDS] fieldsX
      
INNER JOIN FullPathItems fpi
              
ON fpi.ID = fieldsX.ITEMID
      
INNER JOIN [dbo].ITEMS itemsX
              
ON itemsX.ID = fieldsX.FIELDID
      
INNER JOIN [dbo].ITEMS templatesX
              
ON templatesX.ID = fpi.TEMPLATEID

WHERE fieldsX.VALUE LIKE @SEARCHTERM 
AND fpi.ITEMPATH LIKE @FULLPATHSEARCH

/*INCLUDE  THE FOLLOWING LINE TO INCLUDE ITEM ID CONDITION */
-- AND fieldsX.ItemId = 'B3F15B8C-BAE5-40F4-A139-BA7B3EC6E1ED'

/*INCLUDE THE FOLLOWING LINE TO INCLUDE ITEM NAME CONDITION */
-- AND fpi.Name LIKE '%My Item%'

/* INCLUDE THE FOLLOWING LINE TO INCLUDE TEMPLATE NAME CONDITION */
--AND templatesX.Name = 'My Template'

/* INCLUDE THE FOLLOWING LINE TO ADD LANGUAGE CONDITION */
-- AND fieldsX.Language = 'en-US'


ORDER  BY fpi.ITEMPATH



The result includes the following columns:

Sometimes, you'll be presented with several instances of a specific keyword, in which case you can uncomment various lines near the bottom of the script to drill down and filter the results even further.

I find myself using this script on a daily basis and hope that sharing this script helps others track down content quickly and effectively, too.


Friday, August 4, 2017

Sitecore Website Backup Powershell Script

Depending on the environment, infrastructure, and budget - each Sitecore project has their own backup and deployment process.  More recent solutions often include a file backup step during an automated build process - while others take on a more manual process.

Whatever your specific scenario, my rule of thumb for changing anything is: ALWAYS BACKUP BEFORE MAKING CHANGES.

With solutions using less complex deploy processes - I used to select, copy, and paste specific files and directories to a separate location in case something went completely awry.  To save time - and maintain peace of mind before making file changes to a staging or production environment - I now utilize a handy backup script using Powershell that backs up all necessary files (Robocopy) by simply running it.


To use it:

  1. Modify and save the Settings.xml file with your own source and destination paths.

  2. Double-click on RunBackup.bat file.  Once you accept the prompt to run the script as an administrator, you'll see the output of each file being transferred to the destination folder.

  3. Watch it run!

The script explicitly excludes the following Sitecore specific paths that often aren't changed from build to build - and don't need to take up more space than needed.  To add/remove an excluded entries,  open and modify the _sitecorebackupscript.ps1 file's '$exclude' variable.
"_DEV", "App_Data", "Content", "temp", "upload", "sitecore", "sitecore modules" 


Settings.xml - Configuration settings

 <?xml version="1.0" encoding="UTF-8"?>  
 <settings>  
      <!-- Date format for Destination folder -->  
      <DateFormat>MM-dd-yyyy hhmmss</DateFormat>  
      <!-- Source Website directory to backup (exclude trailing back-slash) -->  
      <SourceDirectory>C:\inetpub\wwwroot\Sitecore8\Website</SourceDirectory>  
      <!-- Destination directory where backup made (exclude trailing back-slash) -->  
      <BackupDirectory>C:\inetpub\wwwroot-backup\Sitecore8</BackupDirectory>  
 </settings>  

_sitecorebackupscript.ps1: The Powershell script

 <# Sitecore Backup Script - Gabriel Streza (sitecoregabe.com)   
 **************************************************************************  
 Powershell script that backups your Sitecore website while excluding unessesary files.  
   
 Modify Settings.xml to control Source and Directory paths.   
   
 **************************************************************************  
 #>  
   
 # Directory definitions  
 $scriptpath = $MyInvocation.MyCommand.Path  
 $directory = Split-Path $scriptpath  
 [xml]$ConfigFile = Get-Content "$directory\Settings.xml"  
   
 # Script Variable Defintions  
 $DateFormat = Get-Date -format $ConfigFile.Settings.DateFormat  
 $SourceDirectory = $ConfigFile.Settings.SourceDirectory  
 $BackupDirectory = $ConfigFile.Settings.BackupDirectory  
 $dest = $BackupDirectory + '_' + $DateFormat  
   
 # Folders to exclude  
 $exclude = "_DEV", "App_Data", "Content", "temp", "upload", "sitecore", "sitecore modules"  
   
 # Run as Administrator  
 if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Start-Process powershell.exe "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs; exit }  
   
 # File copy  
 robocopy $SourceDirectory $dest /s /xj /xd $exclude  
   
 # Completed  
 Write-Host -NoNewLine 'DONE! Press any key to continue...';  
 $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown');  

RunBackup.bat: Execute the Powershell script

 Powershell.exe -executionpolicy remotesigned -File "_sitecorebackupscript.ps1"  


You can download, modify, and utilize this by heading to this GitHub repo and grabbing a copy:
https://github.com/strezag/Sitecore-Website-Backup-Script/


Happy backup...ing!

Tuesday, March 28, 2017

ItemWebApi: maxJsonLength Property Exceeded

Sitecore's Item Web API  is extremely useful is so many ways - however as your database grows, you may encounter a limit on the size of the response Sitecore is able to return.

After installing the Sitecore Item Web API  feature to an existing Sitecore v6.6 site, I encountered an error when querying a huge part of the content tree using the following request:
/-/item/v1/?sc_database=master&scope=s&query=/sitecore/content//*

Due to the sheer amount of data the request attempted to retrieve, I received the following response:

"Error during serialization or deserialization using the JSON JavaScriptSerializer.
The length of the string exceeds the value set on the maxJsonLength property."


I initially figured the solution would be as simple as setting the MaxJsonLength property in the web.config, but soon realized that this setting had no real positive effect.

DISCOVERY

At this point, I took it in upon myself to decompile/reflect the Sitecore.ItemWebApi.dll in hopes of overriding the serialization method in order to set the MaxJsonLength property when initializing the JavaScriptSerializer.  

After some digging, I discovered two relevant methods I needed to override:

JsonSerializer

SerializeResponse

The SerializeResponse method serializes the API's response using JsonSerializer.Serialize to return the response in JSON format.

The Sitecore.ItemWebApi.config patch file defines the SerializeResponse pipeline:

SOLUTION

I started a new Website Project in Visual Studio, then
  1. Added the /App_Config/Include/zzz/ folders
  2. Copied over relevant binary files into a /References folder
    • Sitecore.ItemWebApi.dll
    • Sitecore.Kernel.dll (for Sitecore 6.6 - use your site's specific version*)
    • Newtonsoft.Json.dll (v3.5 for Sitecore 6.6  - use your site's specific version*)
  3. And added an Override folder to house the overriding code:



To override the existing Sitecore.ItemWebApi.Pipelines.Request.SerializeResponse definition, I created a patch file called zWebItemApiSerializedResponse.config and added it to the /App_Config/Include/zzz/ folder:

 <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">  
      <sitecore>  
          <pipelines>  
              <itemWebApiRequest>  
                <processor patch:instead="*[@type='Sitecore.ItemWebApi.Pipelines.Request.SerializeResponse, Sitecore.ItemWebApi']" type="Sitecore.SharedSource.ItemWebApiCustom.Override.SerializeResponseOverride, Sitecore.SharedSource.ItemWebApiCustom" />   
              </itemWebApiRequest>  
           </pipelines>  
      </sitecore>  
 </configuration>  

In the Override folder, I created two class files based on the decompiled Sitecore.ItemWebApi.dll  code:

SerializerOverride - inherits ISerializer
 using System;  
 using System.Web.Script.Serialization;  
 using Newtonsoft.Json;  
 using Sitecore.Diagnostics;  
 using Sitecore.ItemWebApi.Serialization;  
 using Formatting = System.Xml.Formatting;  
 namespace Sitecore.SharedSource.ItemWebApiCustom.Override  
 {  
   public class SerializerOverride : ISerializer  
   {  
     public string Serialize(object value)  
     {  
       Assert.ArgumentNotNull(value, "value");  
       var serializer = new JavaScriptSerializer {MaxJsonLength = Int32.MaxValue};  
       dynamic parsedJson = JsonConvert.DeserializeObject(serializer.Serialize(value));  
       return JsonConvert.SerializeObject(parsedJson, (Newtonsoft.Json.Formatting) Formatting.Indented);  
     }  
     public string SerializedDataMediaType => "application/json";  
   }  
 }  


SerializeResponseOverride - inherits SerializeResponse
 using Sitecore.Diagnostics;  
 using Sitecore.ItemWebApi.Pipelines.Request;  
 namespace Sitecore.SharedSource.ItemWebApiCustom.Override  
 {  
   public class SerializeResponseOverride : SerializeResponse  
   {  
     public override void Process(RequestArgs arguments)  
     {  
       Assert.ArgumentNotNull(arguments, "arguments");  
       SerializerOverride serializer = new SerializerOverride();  
       arguments.ResponseText = serializer.Serialize(arguments.Result);  
     }  
   }  
 }  


Once this project successfully built, I copied over the new App_Config file, and the projects Sitecore.SharedSource.ItemWebApiCustom.dll file to my solution's /bin folder (add the .pdb file to the /bin folder if you want to debug!)

RESULT

You can confirm your patch file is properly overriding the original SerializeResponse pipeline using /sitecore/admin/showconfig.aspx and searching for SerializeResponse on the page.

If everything went well, you can refresh the API call to see that the JSON response size restriction has been lifted!

Let me know in the comments if you have any suggestions or issues with this approach.
Also feel free to drop a line if this post has helped you out!



Thursday, March 9, 2017

Sitecore Performance & Troubleshooting Tools

As a consultant within a Managed Services practice, I am often tasked to dig into ancient developer code in search of bottlenecks. Discovering performance issues on Sitecore builds can be tricky because each site you may work on follow different development patterns.

I found that the most common issues only surface after accumulating data, where some components may not have been tested using more than a few items of data.  The fix often ends up being some modification to specific Sitecore queries, or perhaps changing the way data is retrieved from Sitecore (switching to pull from a Lucene index).  Whatever it is, finding the bottleneck is just the first step.

Luckily for us, there are plenty of tools available that make troubleshooting and pin-pointing these issues a thousand times easier.  Regardless of Sitecore version you're working with, here are four performance / troubleshooting tools I often fire up from my arsenal to get to the bottom of slow load times.

SITECORE LOG ANALYZER

This is a given!  If something odd is going on in your Sitecore website, one of the first places to look for clues is the Sitecore logs.  This handy tool developed by Sitecore loads the entire Sitecore log folder and allows you to filter by date, string, message type (ERROR, INFO, DEBUG, etc).    This is often much easier than sifting through flat .txt files.



https://marketplace.sitecore.net/en/Modules/Sitecore_Log_Analyzer.aspx

SITECORE DEBUG TOOL

Built right into Sitecore - I only started utilizing this tool a few years into my career working with Sitecore, but it's been a life saver in a few instances.  My guess is that devs often forget that this exists.  Some features include full trace information for each rendering and profiling - which comes in handy when attempting to identify bottlenecks.  If you're trying to identify which rendering on a page are being pulled from the Sitecore cache, this is one of the best way to find out.

To enable the debugger, the following prerequisites must be satisfied:

  1. Ensure the "allowDebug" and "enableDebugger" attributes for the site are set to true in the site node of the Sites.config.
  2. Ensure you're logged into Sitecore with a user member utilizing the 'sitecore\Sitecore Client Developing' role.



Additional information on how to use the Sitecore debugger can still be found here:
The Sitecore Debugger

APACHE JMETER

Apache JMeter is an open-source Java application which (among other handy features) is great for running load/stress tests and providing the calculated results for analysis.  When configured properly can send multiple HTTP requests to a designated URL location and provides an average load time throughout the increased traffic.  For me, this has been a great way to obtain visualizations of page load time behaviors before and after making caching or other performance enhancement changes.



For more: https://www.digitalocean.com/community/tutorials/how-to-use-apache-jmeter-to-perform-load-testing-on-a-web-server 

 http://jmeter.apache.org/

TELERIK FIDDLER

I'm a big fan of Telerik's suite of developer tools.    Fiddler is one of those tools I always make sure to have installed on my dev box.  If you don't already know, Fiddler logs all HTTP/S traffic between your computer and the web.  This comes in handy when gathering statistics of internal or third-party calls, viewing client headers and/or cookies, viewing results of an API call and more.



http://www.telerik.com/fiddler

TELERIK JUSTTRACE

While JustTrace isn't free, it's definitely one the of easiest .NET profiling tools I've worked with.   Its intuitive tracing mechanism which allows us to identify which .NET components take the most CPU and memory resources which ultimately affect page load times.  In most cases the snapshots provide exact methods to look into and tweak until a more reasonable load time is obtained.




If you haven't tried these tools, I'd recommend playing around with them at some point.  They may become an integral part of your own troubleshooting whether during development or post-production support.  

Have you used any of these in the past?  What are your favorite performance troubleshooting tools?

Let me know in the comments!

Monday, February 6, 2017

Experience Editor Language Flags in Sitecore 8

After upgrading to Sitecore 8, you may notice that the Experience Editor (formally Page Editor) no longer displays flag icons when selecting a language from the Experience Ribbon tab.
For language-heavy sites where multiple language names may be present – this change makes it difficult for Content Authors to distinguish between en-EN, en-US, en-GB, and en-AU, and all other region-based languages while in this mode.

Sitecore 7


Sitecore 8

The absence of the flag icons in Sitecore 8 is reportedly by-design in several areas due to the frequency in changes of flags. Sitecore has opted out of showing any flags to avoid the maintenance involved in keeping the list updated.

Goal

We want the flag indicators back!
But also, the related ISO code.

Our goal is to ensure:
  1. The language flag icon for the Change Language button in the Ribbon is swapped correctly.
  2. The language flag icons are displayed to the left of the Language name.
  3. The language code Iso label is displayed.


To override the default Sitecore request code, we need to determine which files are related to the original feature.
In this particular scenario, we know we’re dealing with the Experience Editor and the Languages chunk in the ribbon.
Inspecting the button element reveals a corresponding request name:
  • data-sc-iconlabelrequest="ExperienceEditor.Language.IconLabel"
  • data-sc-retrievelistitemsrequest="ExperienceEditor.Language.GetLanguageItems"
  • data-sc-require="/-/speak/v1/ribbon/LargeDropDownButton.js
InspectOrginal.png
A quick config search also reveals that the Sitecore.ExperienceEditor.Speak.Requests.config file contains the definition for this (and other related) request(s).
To reach our end result, we’ll need to override the following lines:
SitecoreRequestsConfigDefault.png
These files are related to this enhancement:
  1. \App_Config\Include\Sitecore.ExperienceEditor.Speak.Requests.config
  2. \sitecore\shell\client\Sitecore\Speak\Ribbon\Controls\LargeDropDownButton\LargeDropDownButton.js

Config

To override the language requests, I created a custom config file which should be placed into the App_Config/Include/ folder.

Note that the name of the config file should be prefaced with a ‘z’ so the config can properly patch after the original requests config is rendered: zGetLanguagesExperienceEditor.config
I used “patch:instead” to replace the two requests:

<?xml version="1.0" encoding="UTF-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
   <sitecore>
      <sitecore.experienceeditor.speak.requests>
         <!-- ExperienceEditor.Language.IconLabel Overrides -->
         <request patch:instead="request[@type='Sitecore.ExperienceEditor.Speak.Ribbon.Requests.ChangeLanguage.LanguageIconLabelRequest, Sitecore.ExperienceEditor.Speak.Ribbon']" name="ExperienceEditor.Language.IconLabel" type="ExperienceLanguageFlags.SitecoreExtensions.ExperienceEditor.LanguageIconLabelRequestCustom, ExperienceLanguageFlags" />
         <!-- ExperienceEditor.Language.GetLanguageItems Overrides -->
         <request patch:instead="request[@type='Sitecore.ExperienceEditor.Speak.Ribbon.Requests.ChangeLanguage.GetLanguages, Sitecore.ExperienceEditor.Speak.Ribbon']" name="ExperienceEditor.Language.GetLanguageItems" type="ExperienceLanguageFlags.SitecoreExtensions.ExperienceEditor.GetLanguagesCustom, ExperienceLanguageFlags" />
      </sitecore.experienceeditor.speak.requests>
   </sitecore>
</configuration>

Code

After obtaining the correct name and type, we can utilize a reflection software to grab copies of the GetLanguages and LanguageIconLabelRequest code within the Sitecore.ExperienceEditor.Speak.Ribbon.dll.
I created a new class in a location within our solution, gave it a relevant name, inherited from the original classes, and copied the code structure from the reflection in order to customize it.
  1. GetLanguagesCustom.cs
  2. LanguageIconLabelRequestCustom.cs

Requests.ChangeLanguage.GetLanguages

When you click on the button to /-/speak/request/v1/expeditor/ExperienceEditor.Language.GetLanguageItems and builds out the following JSON response:
FiddlerGetLanguageItemsDefault.png
This response already contains a path to a flag image file - ‘IconPath’ - however, there is no label available for us to utilize on the front-end.
We need to add a new variable for the language code label:


Within my custom GetChildItems() method used to provide the list of Languages in JSON format, I added the LanguageCode variable which attempts to get and set the “Regional Iso Code” (or “Iso” if not available) field from the language item in Sitecore.
GetLanguagesClickDebug.png


The request now includes the LanguageCode field and value:
FiddlerGetLanguageItemsAltered.png


Requests.ChangeLanguage.LanguageIconLabelRequest

I noticed that the flag icon does not change according to the selected language by default.
In the screenshot below, I’ve selected French and while the URL reflects the language change, the flag remains set to the original US icon.
FrenchWithUSFlag.png



Overriding the Requests.ChangeLanguage.LanguageIconLabelRequest request will allow us to provide the correct language data (Icon and Label) for the current item request:
IconLabelLoadDebug.png




Presentation

The location of the inner Sitecore files responsible for this and made a custom modification to display the flags:
\sitecore\shell\client\Sitecore\Speak\Ribbon\Controls\LargeDropDownButton\LargeDropDownButton.js
This change is specific to the Experience Editor and does not extend to the Content Editor.
  1. Force the showIcon variable to 1 (true).  This variable is used when generating the popup HTML.
JS-forceShowIcon.png

2) Create a new variable for the LanguageCode variable now available from the Requests.ChangeLanguage.GetLanguages request and JSON response:
JS-AddLanguageCode.png

3) Generate HTML elements (icon image, span element to house LanguageCode) and append them into the popup container.
JS-createElements.png




Final Result

Our final result will now display the flag icon, language name, and language Iso code value:
FinalResult.png
You can find the source code used in this post on GitHub or download and install the package.
* Please note that this has only been confirmed using Sitecore 8.0 Update-1.

Let me know what you think of this solution and feel free to leave comments or suggestions!