Tuesday, August 4, 2015

Organizing the Language Menu's Tower of Babel

If you have a site with lots of languages, you've no doubt run into some challenges. A small -- but annoying -- one, is the ordering of the language menus. This is particularly problematic when the site has a large number of configured languages, but any given conten item may only have versions in one or two. It can be a chore slogging through the menu looking for ones that have versions.

This little tidbit allows you to re-order the language menu so that languages that actually have versions will float up to the top of that Tower of Babel.

Finding the pressure point

When I'm making changes to Sitecore's "internals" I try to be as minimally invasive as possible. Unfortunately, this solution is more like a tourniquet than a pressure point. I want to replace the code-beside for one of Sitecore's XML controls (the control that Sitecore uses for drop-down language menus in the content editor). The only way I know to do that is to replace the XML file and change the <CodeBeside> element. Anyone know a good trick to change the code-beside without replacing this file?

Worse, the decompiled code-beside doesn't seem to lend itself to a surgical strike that just changes one little part. Often, you'll find that Sitecore anticipates your need by doing things like having an overridable class fetch an object that does most of the work. You can just inherit from their class and change that one method to suit your needs.Not the case here. We're going to have to implement the whole class just to change one line of code. Such is life.

The control for this menu is located at
/Sitecore/Shell/Applications/Content Manager/Galleries/Languages/Gallery Languages.xml
What we really want to do is make a small change to the associated code-beside. First, we need to change this XML file to use our code-beside instead of Sitecore's:

    <!--<CodeBeside Type="Sitecore.Shell.Applications.ContentManager.Galleries.Languages.GalleryLanguagesForm,Sitecore.Client"/>-->  
    <CodeBeside Type="MySolution.Shell.Applications.ContentManager.Galleries.GalleryLanguagesForm,sb1"/> 

For the code-beside file, we need to copy the entire decompiled Sitecore class, although we're really just changing one line of code. So use your favorite decompiler to snag the Sitecore class (or copy the code from the end of this article), and clean up the references.

I'm going to change that one pesky line to call a new method, just to isolate the change and make it more tweakable in the future.

To fetch the list of languages to put in the menu, Sitecore just does a  GetLanguages(currentItem).


That works, but I want to take the languages for which there are actually versions, and float them to the top.


So I change their code to call my method instead of GetLanguages:

 foreach (Language language in GetLanguages(currentItem))  //currentItem.Languages)  

... and then I add the GetLanguages() method that lets me be a it more finessed about ordering:

 protected IEnumerable<Language> GetLanguages(Item {  
  return currentItem.Languages.Where(l => ItemManager.GetVersions(currentItem, l).Count > 0)  
   .Union(currentItem.Languages.Where(l => ItemManager.GetVersions(currentItem, l).Count == 0));  
 }  

And now the languages that are actually used for this item will float to the top. This might be a handy place to do other manipulation you might need for your solution, depending on the business rules for language management.

Here's the full code for the XML control and the code-beside:

 using System;  
 using System.Collections.Generic;  
 using System.Globalization;  
 using System.Linq;  
 using Sitecore;  
 using Sitecore.Configuration;  
 using Sitecore.Data;  
 using Sitecore.Data.Items;  
 using Sitecore.Data.Managers;  
 using Sitecore.Diagnostics;  
 using Sitecore.Globalization;  
 using Sitecore.Shell;  
 using Sitecore.Web;  
 using Sitecore.Web.UI.HtmlControls;  
 using Sitecore.Web.UI.Sheer;  
 using Sitecore.Web.UI.XmlControls;  
 using Control = System.Web.UI.Control;  
 namespace MySolution.Shell.Applications.ContentManager.Galleries  
 {  
   public class GalleryLanguagesForm : Sitecore.Shell.Applications.ContentManager.Galleries.GalleryForm  
   {  
     protected GalleryMenu Options;  
     protected Scrollbox Languages;  
     public GalleryLanguagesForm() : base()  
     {  
     }  
     public override void HandleMessage(Message message)  
     {  
       Assert.ArgumentNotNull((object)message, "message");  
       if (message.Name == "event:click")  
         return;  
       this.Invoke(message, true);  
     }  
     protected override void OnLoad(EventArgs e)  
     {  
       Assert.ArgumentNotNull((object)e, "e");  
       base.OnLoad(e);  
       if (Context.ClientPage.IsEvent)  
         return;  
       Item currentItem = GetCurrentItem();  
       if (currentItem == null)  
         return;  
       using (new ThreadCultureSwitcher(Context.Language.CultureInfo))  
       {  
         foreach (Language language in GetLanguages(currentItem))  //currentItem.Languages)  
         {  
           ID languageItemId = LanguageManager.GetLanguageItemId(language, currentItem.Database);  
           if (!ItemUtil.IsNull(languageItemId))  
           {  
             Item obj = currentItem.Database.GetItem(languageItemId);  
             if (obj == null || !obj.Access.CanRead() || obj.Appearance.Hidden && !UserOptions.View.ShowHiddenItems)  
               continue;  
           }  
           XmlControl xmlControl = ControlFactory.GetControl("Gallery.Languages.Option") as XmlControl;  
           Assert.IsNotNull((object)xmlControl, typeof(XmlControl));  
           Context.ClientPage.AddControl((Control)this.Languages, (Control)xmlControl);  
           Item obj1 = currentItem.Database.GetItem(currentItem.ID, language);  
           if (obj1 != null)  
           {  
             int length = obj1.Versions.GetVersionNumbers(false).Length;  
             string str1;  
             if (length != 1)  
               str1 = Translate.Text("{0} versions.", (object)length.ToString());  
             else  
               str1 = Translate.Text("1 version.");  
             string str2 = str1;  
             CultureInfo cultureInfo = language.CultureInfo;  
             xmlControl["Header"] = (object)(cultureInfo.DisplayName + " : " + cultureInfo.NativeName);  
             xmlControl["Description"] = (object)str2;  
             xmlControl["Click"] = (object)string.Format("item:load(id={0},language={1},version=0)", (object)currentItem.ID, (object)language);  
             xmlControl["ClassName"] = !language.Name.Equals(WebUtil.GetQueryString("la"), StringComparison.OrdinalIgnoreCase) ? (object)"scMenuPanelItem" : (object)"scMenuPanelItemSelected";  
           }  
         }  
       }  
       Item obj2 = Sitecore.Client.CoreDatabase.GetItem("/sitecore/content/Applications/Content Editor/Menues/Languages");  
       if (obj2 == null)  
         return;  
       this.Options.AddFromDataSource(obj2, string.Empty);  
     }  
     protected IEnumerable<Language> GetLanguages(Item currentItem)  
     {  
       return currentItem.Languages.Where(l => ItemManager.GetVersions(currentItem, l).Count > 0)  
         .Union(currentItem.Languages.Where(l => ItemManager.GetVersions(currentItem, l).Count == 0));  
     }  
     private static Item GetCurrentItem()  
     {  
       string queryString1 = WebUtil.GetQueryString("db");  
       string queryString2 = WebUtil.GetQueryString("id");  
       Language language = Language.Parse(WebUtil.GetQueryString("la"));  
       Sitecore.Data.Version version = Sitecore.Data.Version.Parse(WebUtil.GetQueryString("vs"));  
       Database database = Factory.GetDatabase(queryString1);  
       Assert.IsNotNull((object)database, queryString1);  
       return database.GetItem(queryString2, language, version);  
     }  
   }  
 }  


 <?xml version="1.0" encoding="utf-8" ?>  
 <control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense" xmlns:shell="http://www.sitecore.net/shell">  
  <Gallery.Languages>  
   <Gallery>  
    <!--<CodeBeside Type="Sitecore.Shell.Applications.ContentManager.Galleries.Languages.GalleryLanguagesForm,Sitecore.Client"/>-->  
    <CodeBeside Type="sb1.Shell.Applications.ContentManager.Galleries.GalleryLanguagesForm,sb1"/>  
    <Script>  
     window.onload = function() {  
     var activeLanguage = document.querySelector('.scMenuPanelItemSelected');  
     activeLanguage.scrollIntoView(false);  
     }  
    </Script>  
    <Stylesheet Key="GalleryLanguages">  
     .scMenuPanelItem, .scMenuPanelItem_Hover, .scMenuPanelItemSelected_Hover, .scMenuPanelItemSelected {  
     padding-left: 0;  
     padding-right: 0;  
     padding-top: 8px;  
     padding-bottom: 8px;  
     }  
     .scGalleryGrip {  
     position: absolute;  
     bottom: 1px;  
     left: 1px;  
     right: 1px;  
     height: 10px;  
     }  
     .scLanguagesGalleryMenu {  
     overflow: hidden;  
     vertical-align: top;  
     border-bottom: 12px solid transparent;  
     -moz-box-sizing: border-box;  
     box-sizing: border-box;  
     width: 100%;  
     height: 100%;  
     border-collapse: separate;  
     }  
     div#Languages img {  
     display: none;  
     }  
    </Stylesheet>  
    <Border Width="100%" Height="100%">  
     <GalleryMenu ID="Options" Class="scLanguagesGalleryMenu">  
      <MenuPanel Height="100%">  
       <Scrollbox ID="Languages" Class="scScrollbox scFixSize scFixWidthInsideGallery" style="padding-top:0 !important;" Height="100%" Width="100%" />  
      </MenuPanel>  
     </GalleryMenu>  
     <Gallery.Grip />  
    </Border>  
   </Gallery>  
  </Gallery.Languages>  
 </control>