Sitecore Adaptive Images – Huge Crawling Logs Problem

Recently we noticed that on our production environment the Crawling Log files were abnormally big – between 500 MB and 1 GB. After some digging we found out that they were just filling up with exceptions while crawling the media library. The exceptions look like this:


4576 10:48:07 WARN Could not compute value for ComputedIndexField: urllink for indexable: sitecore://master/{8EA15044-EE2C-41DB-81D6-0A9C42062814}?lang=en&ver=1
Exception: System.NullReferenceException
Message: Object reference not set to an instance of an object.
Source: Sitecore.Kernel
at Sitecore.Context.PageMode.get_IsNormal()
at adaptiveImages.AdaptiveImagesMediaProvider.GetMediaUrl(MediaItem item)
at Sitecore.ContentSearch.ComputedFields.UrlLink.ComputeFieldValue(IIndexable indexable)
at Sitecore.ContentSearch.LuceneProvider.LuceneDocumentBuilder.AddComputedIndexFields()

Considering the case that we have tons of media (over 2000 items) and languages (there is a separate exception for each language) we had some pretty serious index time performance issues and hard disk space problems.

After some debugging we found out that the Sitecore.Context.PageMode.IsNormal was throwing null reference. The huge problem was that there is no way to protect against null pointers here because you cannot check the Sitecore.Context and the Sitecore.Context.PageMode for nulls. The good part is that the PageMode.IsNormal method code is using the Sitecore.Context.Site (which is the actual null in this case) to check the PageMode.


public static bool IsNormal
{
    get
    {
        return Context.Site.DisplayMode == DisplayMode.Normal;
    }
}

With this information the fix became pretty trivial. As try-catch is pretty expensive operation – we just needed to check if the Context.Site is not null and the Context.Site.DisplayMode is normal. The fixes took place in the two GetMediaUrl overrides. Here is the code for the modified methods which were causing the exception.

  /// <summary>
        /// Gets a media URL.
        /// </summary>
        /// <param name="item">The media item.</param>
        /// <returns>
        /// The media URL.
        /// </returns>
        public override string GetMediaUrl(MediaItem item)
        {
            Assert.ArgumentNotNull(item, "item");

            // FIX FOR CHECKING THE PAGE MODE
            bool isNormal = Context.Site != null && Context.Site.DisplayMode == DisplayMode.Normal;

            //If media item is not an image or the page context is not normal, then return
            if (!IsImage(item) || !isNormal)
                return base.GetMediaUrl(item);

            MediaUrlOptions mediaUrlOptions = new MediaUrlOptions();

            return GetMediaUrl(item, mediaUrlOptions);
        }

        /// <summary>
        /// Gets the media URL.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <param name="mediaUrlOptions">The media URL options.</param>
        /// <returns></returns>
        public override string GetMediaUrl(MediaItem item, MediaUrlOptions mediaUrlOptions)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNull(mediaUrlOptions, "mediaUrlOptions");

            // FIX FOR CHECKING THE PAGE MODE
            bool isNormal = Context.Site != null && Context.Site.DisplayMode == DisplayMode.Normal;

            //If media item is not an image or the page context is not normal, then return
            if (!IsImage(item) || !isNormal || Context.Database == null || Context.Database.Name != _database)
                return base.GetMediaUrl(item, mediaUrlOptions);

            //If resolution cookie is not set
            if (!IsResolutionCookieSet())
            {
                //If mobileFirst is set to FALSE or user agent is identifying as a desktop, return with largest break-point resolution
                if (!_mobileFirst || IsDesktopBrowser())
                {
                    mediaUrlOptions.MaxWidth = GetLargestBreakpoint();
                    return base.GetMediaUrl(item, mediaUrlOptions);
                }
                //Return with mobile-first breakpoint (Smallest)
                mediaUrlOptions.MaxWidth = GetMobileFirstBreakpoint();
                return base.GetMediaUrl(item, mediaUrlOptions);
            }

            // If Max-width is not set or Max-width is greater than the selected break-point, then set the Max-width to the break-point
            if (mediaUrlOptions.MaxWidth == 0 || mediaUrlOptions.MaxWidth > GetScreenResolution())
                mediaUrlOptions.MaxWidth = GetScreenResolution();

            // If Max-width is not set and the 'maxWidth' setting is not empty, then set the Max-width property to the maxWidth
            if (mediaUrlOptions.MaxWidth == 0 && !string.IsNullOrEmpty(_maxWidth))
            {
                int maxWidth = 0;
                if (int.TryParse(_maxWidth, out maxWidth))
                {
                    // If pixel ratio is normal
                    if (GetCookiePixelDensity() == 1)
                        mediaUrlOptions.MaxWidth = maxWidth;
                    else
                        mediaUrlOptions.MaxWidth = maxWidth * GetCookiePixelDensity();
                }

            }

            return base.GetMediaUrl(item, mediaUrlOptions);
        }

Basically we are using an isNormal variable to check the Context.Site display mode instead of using the Context.PageMode.IsNormal property:


            bool isNormal = Context.Site != null && Context.Site.DisplayMode == DisplayMode.Normal;

By doing this we kept the module’s consistency and logic as we just added a null check for the Context.Site. After the fix our logs became around 2 kb again and our indexing speed came back to normal !

Happy Log Fixing ! 🙂

 

5 Reasons To Pick Active Commerce For Your E-Commerce Needs

Recently the guys at Active Commerce (Follow @activecommerce) provided a free development training for Sitecore MVPs led by Nick Wesselman (Follow @techphoria414). The guys there did an amazing job in implementing a really simple yet powerful E-Commerce solution for Sitecore, so I decided to write a blog post about why you should pick Active Commerce as your E-Commerce platform.

Reason 1 –  It is built on top of Sitecore with Sitecore ! 

One of the best things about Active Commerce is that it uses all the built-in Sitecore Capabilities for creating an E-Commerce solution. Active Commerce uses and extends the Sitecore Layout Engine by adding skinning capabilities (which will be covered in Reason 2). It uses and extends Sitecore Ecommerce Services (SES) for E-Commerce related things like product catalogs, shopping carts, etc. Also it ships with already build templates for common things like product, product variants, filters and so on.

Reason 2 – Shapeshifting (more known as Active Commerce Skinning System)

Active Commerce ships with already built frontend (which is in fact pretty good and responsive). The bad part about this is that our clients don`t want to be mainstream and they want their own brand “look and feel”. The good part about this is that the guys at Active Commerce know that ! The Skinning System is an extension of the existing Sitecore Layout Engine, which allows the developers to utilize the existing skin and just replace the parts which need modification (including styles, html templates, scripts, images etc.). So here is a brief overview of the frontend goodies Active Commerce utilizes to make our lives easier:

  1. Cassete for asset management, bundling, minification and bundling.
  2. LESS Preprocessor for styling – which means less development work to modify the existing styles thanks to the variables and mixins less provides.
  3. jQuery-tmpl for HTML Templating – which allows quick markup changes thanks to the dynamic generation of the HTML Templates.
  4. jQuery for javascript library – nothing to say here – even a frontend newbie like me is familiar with jQuery :).
  5. Special mobile-optimized templates and behaviours (Using the Sitecore Device Layer).

Reason 3 – Easy to Configure

As already stated in Reason 1 – Active Commerce is built to work seamlessly with Sitecore, which means that it utilizes the already existing Sitecore config patching strategy we are used to. If you want to extend or just modify something Active Commerce related there are patch files which are self-descriptive (and well commented :)). Of course Active Commerce is not configurable solely by configuration files, there is a pretty robust system for site configuration inside the Content Tree. From inside the content tree your editors will be able to configure everything starting from basic contact data of the company through tax calculators, default currencies, etc.

Reason 4 – Inversion of Commerce (or Control :)) and Major Extension Points

Active Commerce is easily extendable thanks to the fact that it uses Microsoft Unity as dependency injection container. Unity makes it easier for the developers to replace existing functionality, extend the existing components and add their own custom logic on top. From data modeling standpoint – Active Commerce uses Glass Mapper as ORM which makes it even easier to extend the models and create custom types for our own data models – the only thing necessary is to inherit from the ActiveCommerce.Products.Product class.

Reason 5 – Support

Having a good support team is one of the most important things when you want your product to be successful and in my experience I can honestly say the guys in the Active Commerce Support team are amazing. I saw nothing else but fast and proper responses when I had any questions.

Happy E-Commercing ! 🙂

Language Fallback Module: Endless Fallback Problem Fix

Alex Shyba`s Language Fallback is one of the most popular modules for multi-language websites. Thanks to this module it is possible to automatically fallback to another language version of the item if there is no version found in the requested language. The module is proven to work in many Sitecore solutions.

However, there is one serious issue which may cause a lot of problems even in production environments – there is no protection against circular fallbacks. For example there is no protection for language to fallback to itself (A->A->…), or languages to have circular fallback to each other (A->B->A->…), or even more complex cases with more than 3 languages involved (A->B->C->A->…). This scenarios might bring your servers to the ground with nothing else except application pool crash and some strange error in the server error logs.

On the bright side there is a quick fix that can resolve the problem.

First we need to download the Module Source from the Sitecore Marketplace.

After that we need to open the Sitecore.LanguageFallbackItemProvider solution. The fix will take place in LanguageFallbackItemProvider class. Currently the method recursively calls itself until the item is found in the target language or until there is no fallback language defined. You can check the code below.

namespace Sitecore.Data.Managers
{
  using Globalization;

  using Items;

  /// <summary>
  ///
  /// </summary>
  public class LanguageFallbackItemProvider : ItemProvider
  {
    /// <summary>
    ///
    /// </summary>
    /// <param name="itemId"></param>
    /// <param name="language"></param>
    /// <param name="version"></param>
    /// <param name="database"></param>
    /// <returns></returns>
    protected override Item GetItem(ID itemId, Language language, Version version, Database database)
    {
      var item = base.GetItem(itemId, language, version, database);

      if (item == null)
      {
        return item;
      }

      if (item.Versions.GetVersionNumbers().Length > 0)
      {
        return item;
      }

      // there is no version in this language.
      Language fallbackLanguage = LanguageFallbackManager.GetFallbackLanguage(language, database, itemId);
      if (fallbackLanguage == null)
      {
        return item;
      }

      Item fallback = GetItem(itemId, fallbackLanguage, Version.Latest, database);

      var stubData = new ItemData(fallback.InnerData.Definition, item.Language, item.Version, fallback.InnerData.Fields);
      var stub = new StubItem(itemId, stubData, database) { OriginalLanguage = item.Language };
      stub.RuntimeSettings.SaveAll = true;

      return stub;
    }
  }
}

The idea behind the fix is to create a new method which will check if the fallback language equals the current language or if not – to keep a collection of languages which were already tested for fallback value and if the languages start to repeat – to return the item (or null). The fixed code below.



namespace Sitecore.Data.Managers
{
    using Globalization;

    using Items;

    using System.Collections.Generic;

    /// <summary>
    ///
    /// </summary>
    public class LanguageFallbackItemProvider : ItemProvider
    {
        /// <summary>
        ///
        /// </summary>
        /// <param name="itemId"></param>
        /// <param name="language"></param>
        /// <param name="version"></param>
        /// <param name="database"></param>
        /// <returns></returns>
        protected override Item GetItem(ID itemId, Language language, Version version, Database database)
        {
            return GetItem(itemId, language, version, database, new List<Language> { language });

        }

        private Item GetItem(ID itemId, Language language, Version version, Database database,
                             List<Language> fallbackedLanguages)
        {

            var item = base.GetItem(itemId, language, version, database);

            if (item == null)
            {
                return item;
            }

            if (item.Versions.GetVersionNumbers().Length > 0)
            {
                return item;
            }

            Language fallbackLanguage = LanguageFallbackManager.GetFallbackLanguage(language, database, itemId);

            // If the fallback language is null, or the fallback language matches the current language (A-A Reference), or if the language was already tested for fallback value (A-B-... Reference) - Return the item (null).
            if (fallbackLanguage == null || fallbackLanguage == language || fallbackedLanguages.Contains(fallbackLanguage))
            {
                return item;
            }

            //Add the fallback language to the collection
            fallbackedLanguages.Add(fallbackLanguage);

            Item fallback = GetItem(itemId, fallbackLanguage, Version.Latest, database, fallbackedLanguages);

            var stubData = new ItemData(fallback.InnerData.Definition, item.Language, item.Version,
                                        fallback.InnerData.Fields);
            var stub = new StubItem(itemId, stubData, database) { OriginalLanguage = item.Language };
            stub.RuntimeSettings.SaveAll = true;

            return stub;
        }

    }
}

And that is it ! Now you can sleep well knowing that even if someone messes up with the language fallbacks the server won`t go down !

Happy Fallbacking ! 🙂

SPEAK UI Filterable TreeView

While testing some Sitecore 7.5 features and reviewing the Sitecore 8 Videos it is obvious that SPEAK UI is quickly becoming the new trend for developing Sitecore Modules. Recently I stumbled upon the following SDN Thread – SPEAK UI – Filter TreeView. It turns out that the existing TreeView doesn’t support features like filtering by Template or Item ID. On the bright side – every SPEAK Control can be extended pretty easy.

NOTE: You must have Sitecore Rocks Installed To Develop SPEAK Modules (and you need to have it in general, because it is awesome !). If you don`t have Sitecore Rocks Installed, please visit this place first to find some tutorials and the extension itself.

NOTE: For the purposes of this blog post I am going to use the following technologies – Visual Studio 2013, Sitecore 7.5, Sitecore Rocks 1.2.6.0

Here are the 3 easy steps to create a Filterable TreeView.

Step 1 – Setup the Visual Studio Project

Create a new ASP.NET Web Application Project (I am going to name it TreeViewWithFilters). It should use the empty project template with folders and core references for MVC. If you are not sure on how to do this please see the screenshot bellow.

Project Setup

After the project is created delete everything from it and create the following folder structure – /sitecore/shell/client/Business Component Library/Layouts/Renderings/ListsAndGrids/TreeViews. Your project should look like the screenshot bellow.

Tree View With Filters

Set the System.Web.Mvc Reference`s Copy Local Property to False (for some strange reason it is conflicting with the Sitecore Version despite the fact it is using the same version that Sitecore uses).

From your local Sitecore Instance add references to Sitecore.Kernel, Sitecore.Mvc, Sitecore.Speak.Client, Sitecore.Speak.Mvc (I usually keep the files in a folder named “Components” inside the solution root) with Copy Local set to False.

With this the project is all set and we can start hacking !

Step 2 – Create the Custom Component

Under the TreeViews folder create a new SPEAK Component with JavaScript. Place it under /core/sitecore/client/Business Component Library/Layouts/Renderings/Lists and Grids in the Content Treer. Your Solution and your Sitecore Content Tree should now look like this:

Initial Creation

After this is set – we need to create the Rendering Parameters. As we are going to extend the existing Tree View – copy and paste the TreeView Parameters (with Rocks you can use Clipboard -> Copy (CTRL + C), Clipboard -> Paste (CTRL + V)) and rename them to TreeViewWithFilters Parameters. After this is set we are going to add a new template section called Filters. The section will have one Single-Line Text Field – ExcludeTemplatesForDisplay (Make sure the field is marked as Shared). The template should look like the screenshot bellow.

Rendering Parameters

After this is set let’s move to the code !

Step 3 – The Codezzz !

Again as we are going to extend the existing TreeView we will reuse all the code there and just do a few small modifications. The original TreeView code files are:

  • [SITECORE_WEB_ROOT\sitecore\shell\client\Business Component Library\Layouts\Renderings\ListsAndGrids\TreeViews\TreeView.cshtml
  • [SITECORE_WEB_ROOT\sitecore\shell\client\Business Component Library\Layouts\Renderings\ListsAndGrids\TreeViews\TreeView.js

TreeViewWithFilter.cshtml Changes

Our TreeViewWithFilter.cshtml should look like this:


@using Sitecore
@using Sitecore.Data.Items
@using Sitecore.Globalization
@using Sitecore.Mvc
@using Sitecore.Mvc.Presentation
@using Sitecore.Resources
@using Sitecore.Web.UI
@using Sitecore.Web.UI.Controls.Common.JqueryUiUserControls
@model RenderingModel
@{
    var userControl = this.Html.Sitecore().Controls().GetJqueryUiUserControl(this.Model.Rendering);
    userControl.Class = "sc-treeview";
    // TreeViewWithFilters Addition
    userControl.Requires.Script("business", "treeviewwithfilters.js");
    var rootItemIcon = string.Empty;
    string rootItemId = userControl.GetString("RootItem");
    string contentLanguage = userControl.GetString("ContentLanguage");
    var selectedItems = userControl.GetString("SelectedItemId");
    var preLoadPath = userControl.GetString("PreLoadPath");

    var checkedItems = userControl.GetString("CheckedItems");
    var clickFolderMode = userControl.GetInt("ClickFolderMode");
    var checkboxEnabled = userControl.GetBool("IsCheckboxEnabled");
    var showScIcons = userControl.GetBool("ShowSitecoreIcons");
    var selectMode = userControl.GetInt("SelectMode");

    Item rootItem = null;
    if (!string.IsNullOrEmpty(rootItemId))
    {
        rootItem = ClientHost.Databases.ContentDatabase.GetItem(rootItemId, Language.Parse(contentLanguage));
    }

    if (rootItem == null)
    {
        rootItem = ClientHost.Databases.ContentDatabase.GetRootItem();
    }

    rootItemIcon = Images.GetThemedImageSource(!string.IsNullOrEmpty(rootItem.Appearance.Icon) ? rootItem.Appearance.Icon : "Applications/16x16/documents.png", ImageDimension.id16x16);
    userControl.SetAttribute("data-sc-rootitem", rootItem.DisplayName + "," + rootItem.Database.Name + "," + rootItem.ID + "," + rootItemIcon);
    userControl.SetAttribute("data-sc-rootitempath", rootItem.Paths.Path);
    userControl.SetAttribute("data-sc-loadpath", preLoadPath);
    userControl.SetAttribute("data-sc-contentlanguage", contentLanguage);

    // TreeViewWithFilters Addition
    userControl.SetAttribute("data-sc-excludetemplatesfordisplay", userControl.GetString("ExcludeTemplatesForDisplay"));

    userControl.AddOptionAttribute("selectedItems");
    //userControl.AddOptionAttribute("preLoadPath");
    userControl.AddOptionAttribute("checkedItems");
    userControl.AddOptionAttribute("clickFolderMode");
    userControl.AddOptionAttribute("selectMode");
    userControl.AddBoolOptionAttribute("IsCheckboxEnabled", "checkbox");
    userControl.AddBoolOptionAttribute("ShowSitecoreIcons");
    userControl.AddBoolOptionAttribute("ShowHiddenItems", "showhiddenitems");

    var htmlAttributes = userControl.HtmlAttributes;
}
<div @htmlAttributes>
    <ul></ul>
</div>

There are 2 minor changes to the original code:

1. Changing the required script to our Javascript file:


    userControl.Requires.Script("business", "treeviewwithfilters.js");

2. Passing the ExcludeTemplatesForDisplay parameter:


 // TreeViewWithFilters Addition
 userControl.SetAttribute("data-sc-excludetemplatesfordisplay", userControl.GetString("ExcludeTemplatesForDisplay"));

TreeViewWithFilter.js Changes

Our TreeViewWithFilter.js should look like this:


/// <reference path="../../../../../../assets/lib/dist/sitecore.js" />
/// <reference path="../../../../../../assets/vendors/underscore/underscore.js" />

require.config({
    paths: {
        dynatree: "/sitecore/shell/client/Speak/Assets/lib/ui/1.1/deps/DynaTree/jquery.dynatree-1.2.4",
        dynatreecss: "/sitecore/shell/client/Speak/Assets/lib/ui/1.1/deps/DynaTree/skin-vista/ui.dynatree",
    },
    shim: {
        "dynatree": { deps: ["jqueryui"/*, 'css!dynatreecss'*/] }
    }
});

define(['sitecore', 'dynatree'], function (_sc) {
    var control = {
        componentName: "TreeView",
        selector: ".sc-treeview",
        control: "dynatree",
        namespace: "ui-",

        attributes:
        [
          { name: "selectedItemId", added: true },
          { name: "selectedItemPath", added: true },
          { name: "checkedItemIds", added: true },
          { name: "pathToLoad", added: true },
          { name: "isBusy", added: true },
          { name: "selectedNode", added: true },
          { name: "showSitecoreIcons", added: true, defaultValue: true },
          { name: "isCheckboxEnabled", pluginProperty: "checkbox", defaultValue: true },     // Show checkboxes.
          { name: "isKeyboardSupported", pluginProperty: "keyboard", defaultValue: true },     // Support keyboard navigation.
          { name: "isPersist", pluginProperty: "persist", defaultValue: false },      // Persist expand-status to a cookie
          { name: "isAutoFocus", pluginProperty: "autoFocus", defaultValue: true },    // Set focus to first child, when expanding or lazy-loading.
          { name: "isAutoCollapse", pluginProperty: "autoCollapse", defaultValue: false }, // Automatically collapse all siblings, when a node is expanded.
          { name: "clickFolderMode", defaultValue: 1 }, // 1:activate, 2:expand, 3:activate and expand
          { name: "selectMode", defaultValue: 3 },      // 1:single, 2:multi, 3:multi-hier
          { name: "isNoLink", pluginProperty: "noLink", defaultValue: false },       // Use <span> instead of <a> tags for all nodes
          { name: "debugLevel", defaultValue: 0 },       // 0:quiet, 1:normal, 2:debug
          { name: "showHiddenItems", defaultValue: false },
          { name: "contentLanguage", defaultValue: "en" },

          // TreeViewWithFilters Addition
          { name: "excludeTemplatesForDisplay", defaultValue: "" }
        ],

        events:
        [
          { name: "onPostInit" },        // Callback(isReloading, isError) when tree was (re)loaded.
          { name: "onActivate", on: "onActivate" },        // Callback(dtnode) when a node is activated.
          { name: "onDeactivate" },      // Callback(dtnode) when a node is deactivated.
          { name: "onSelect", on: "onSelect" },          // Callback(flag, dtnode) when a node is (de)selected.
          { name: "onExpand" },          // Callback(flag, dtnode) when a node is expanded/collapsed.
          { name: "onLazyRead", on: "nodeExpanding" },   // Callback(dtnode) when a lazy node is expanded for the first time.
          { name: "onCustomRender" },    // Callback(dtnode) before a node is rendered. Return a HTML string to override.
          { name: "onCreate" },          // Callback(dtnode, nodeSpan) after a node was rendered for the first time.
          { name: "onRender" },          // Callback(dtnode, nodeSpan) after a node was rendered.
          { name: "postProcess" }        // Callback(data, dataType) before an Ajax result is passed to dynatree.
        ],

        functions:
        [
          { name: "disable" },
          { name: "enable" },
          { name: "getTree" },
          { name: "getRoot" },
          { name: "getActiveNode" },
          { name: "getSelectedNodes" }
        ],

        view:
        {
            initialized: function () {
                var clickFolderModeAttribute = this.$el.attr("data-sc-option-clickfoldermode"),
                  selectModeAttribute = this.$el.attr("data-sc-option-selectmode");

                this.model.set("clickFolderMode", clickFolderModeAttribute && !isNaN(clickFolderModeAttribute) ? parseInt(clickFolderModeAttribute, 10) : 1);
                this.model.set("selectMode", selectModeAttribute && !isNaN(selectModeAttribute) ? parseInt(selectModeAttribute, 10) : 3);
                this.model.set("contentLanguage", this.$el.attr("data-sc-contentlanguage"));
                this.model.set("showHiddenItems", this.$el.attr("data-sc-option-showhiddenitems") !== undefined && this.$el.attr("data-sc-option-showhiddenitems").toLowerCase() === "true");

                // TreeViewWithFilters Addition
                this.model.set("excludeTemplatesForDisplay", this.$el.attr("data-sc-excludetemplatesfordisplay"));

                var items = this.$el.attr("data-sc-rootitem");
                if (!items) {
                    return;
                }

                var pathToLoad = this.$el.attr("data-sc-loadpath");

                if (pathToLoad) {
                    this.model.set("pathToLoad", pathToLoad);
                }

                var parts = items.split("|");
                var selectedItemUri = null;

                for (var n = 0; n < parts.length; n++) {
                    var uri = parts[n].split(',');

                    var databaseUri = new _sc.Definitions.Data.DatabaseUri(uri[1]);
                    var itemUri = new _sc.Definitions.Data.ItemUri(databaseUri, uri[2]);
                    var itemIcon = uri[3];

                    var rootNode = {
                        title: uri[0],
                        key: itemUri.getItemId(),
                        isLazy: true,
                        isFolder: true,
                        icon: this.model.get("showSitecoreIcons") ? itemIcon : "",
                        url: "#",
                        path: "/" + uri[0],
                        itemUri: itemUri,
                        selected: selectedItemUri == null
                    };

                    if (this.widget) {
                        this.widget.apply(this.$el, ["getRoot"]).addChild(rootNode);
                    }

                    if (this.model.get("pathToLoad") && this.model.get("pathToLoad") !== "") {
                        this.loadKeyPath();

                    }
                }

                this.pendingRequests = 0;
            },

            onActivate: function (node) {
                if (node && node.data && node.data.itemUri && node.data.path) {

                    this.model.set("selectedNode", node.data);
                    this.model.set("selectedItemId", node.data.itemUri.itemId);
                    this.model.set("selectedItemPath", node.data.path);
                }
            },

            onSelect: function (flag, node) {
                var list = [];

                _.each(node.tree.getSelectedNodes(), function (dnode, index) {
                    list.push(dnode.data.itemUri.itemId);
                });
                this.model.set("checkedItemIds", list.join("|"));
            },

            nodeExpanding: function (node) {

                var itemUri = node.data.itemUri, children;

                this.pendingRequests++;
                this.model.set("isBusy", true);

                var self = this;

                // TreeViewWithFilters Addition
                var excludedTemplatesList = new Array();
                if (this.model.get("excludeTemplatesForDisplay") !== undefined && this.model.get("excludeTemplatesForDisplay") != "") {
                    excludedTemplatesList = this.model.get("excludeTemplatesForDisplay").split("|");
                }

                var database = new _sc.Definitions.Data.Database(itemUri.getDatabaseUri());
                database.getChildren(itemUri.getItemId(), function (items) {
                    var res = [],
                      filteredItems = _.filter(items, function (item) {

                          // TreeViewWithFilters Modification

                          if (self.model.get("showHiddenItems")) {
                              return true && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                          }

                          return item.__Hidden !== "1" && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                      });

                    self.appendLoadedChildren(node, filteredItems, res);

                    self.pendingRequests--;
                    if (self.pendingRequests <= 0) {
                        self.pendingRequests = 0;
                        self.model.set("isBusy", false);
                    }

                    if (self.model.get("pathToLoad") && self.model.get("pathToLoad").length > 0) {
                        self.loadKeyPath();
                    }
                }, {
                    fields: ["__Hidden"],
                    language: self.model.get("contentLanguage")
                });

            },

            appendLanguageParameter: function (item) {
                if (item.$icon.indexOf(".ashx") > 0) {
                    item.$icon += "&la=" + this.model.get("contentLanguage");
                    item.$mediaurl += "&la=" + this.model.get("contentLanguage");
                }
            },

            appendLoadedChildren: function (parentNode, childrenNodes, destArray) {
                //add children

                var self = this;
                _.each(childrenNodes, function (item) {
                    var newNode = {};
                    newNode.rawItem = item;
                    newNode.title = item.$displayName;
                    newNode.key = item.itemId;
                    if (self.model.get("showSitecoreIcons")) {
                        self.appendLanguageParameter(item);
                        newNode.icon = item.$icon;
                    }
                    newNode.itemUri = item.itemUri;
                    newNode.path = item.$path;
                    newNode.select = self.model.get("selectMode") === 3 ? parentNode.isSelected() : false;
                    newNode.isFolder = item.$hasChildren;
                    newNode.isLazy = item.$hasChildren;
                    destArray.push(newNode);

                }, this);

                parentNode.setLazyNodeStatus(DTNodeStatus_Ok);
                parentNode.addChild(destArray);
                //expand needed node
            },
            loadKeyPath: function () {
                var separator = "/",
                    pathParts,
                    currentNodeId,
                    path = this.model.get("pathToLoad"),
                    tree = this.widget.apply(this.$el, ["getTree"]),
                    node;

                pathParts = path.split(separator);
                if (pathParts.length === 0) {
                    return false;
                }
                else {
                    currentNodeId = pathParts.shift();
                    if (!currentNodeId) return false;
                    node = tree.getNodeByKey(currentNodeId);
                    if (!node) {
                        this.model.set("pathToLoad", "");

                        return false;
                    }

                    this.model.set("pathToLoad", pathParts.join(separator));
                    node.expand();

                    if (pathParts.length === 0) {
                        this.model.set("selectedItemId", currentNodeId);
                        node.activate(true);
                        node.select(true);
                    }

                }
            }
        }
    };

    _sc.Factories.createJQueryUIComponent(_sc.Definitions.Models, _sc.Definitions.Views, control);
});

The method is modified to do the following.

1. Register our new attribute with the model.

   // TreeViewWithFilters Addition
          { name: "excludeTemplatesForDisplay", defaultValue: "" }

2. Load the value we passed in the rendering

         // TreeViewWithFilters Addition
                this.model.set("excludeTemplatesForDisplay", this.$el.attr("data-sc-excludetemplatesfordisplay"));

3. Override the nodeExpanding method in order for it to filter based on our custom filter.

        // TreeViewWithFilters Addition
                var excludedTemplatesList = new Array();
                if (this.model.get("excludeTemplatesForDisplay") !== undefined && this.model.get("excludeTemplatesForDisplay") != "") {
                    excludedTemplatesList = this.model.get("excludeTemplatesForDisplay").split("|");
                }

                var database = new _sc.Definitions.Data.Database(itemUri.getDatabaseUri());
                database.getChildren(itemUri.getItemId(), function (items) {
                    var res = [],
                      filteredItems = _.filter(items, function (item) {

                          // TreeViewWithFilters Modification

                          if (self.model.get("showHiddenItems")) {
                              return true && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                          }

                          return item.__Hidden !== "1" && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                      });

And this is it !

The component can be found in the standard Speak Rendering selector as seen on the screenshot bellowTree View With Filters

After the component is included in the layout the custom field can be edited from the component properties.

Tree View With Filters Properties

Currently the code supports only TemplateIDs, but it can be modified to support template keys etc.

The code is also pushed to Bitbucket along with some extra filters – ExcludeItemsForDisplay and IncludeTemplatesForDisplay. Adding new filters is just a matter of modifying the Parameter templates, handling them in the rendering and modifying the nodeExpanding method in order for it to support your custom type of filter.

Happy Speaking !

Sitecore Kicking Idle Users

Sitecore Licenses are often based on the number of users simultaneously logged in the Sitecore Client. If the threshold of maximum logged in users is reached the next user that tries to login gets the following screen:

Over Capactiy

Here the current user has 2 choices:

  1. Temporary boost the amount of concurrent users – in case the current user has boost permissions and the instance is not using express license.
  2. Kick another user who is already logged in – in case the current user has kick permissions.

Boosting will take the user to http://sitecore.net page from where he can temporarily increase the number of users who can be simultaneously logged in the system.

Kicking will take the current user to the following page.

Kick Users

The page lists the currently logged in users. The time they logged in and the Last Request Time is shown as well. From here the current user can decide if he wants to kick any of the currently logged in users.

The nice part is that this page can also be accessed directly even if the Sitecore instance is not over capacity. The page can be accessed from here: http://[SITECORE_INSTANCE_URL]/sitecore/shell/Applications/Login/Users/Kick.aspx and comes in handy when the instance becomes slower from the many idle users that forgot to logout.

Happy Kicking ! 🙂

The Amazing World of Razl – Part 1

As a Hedgehog employee I have the pleasure of playing around with all of their tools. I find it strange that many Sitecore developers haven`t realized the potential of Razl. Most of us consider it a DB compare tool which we can only use once or twice in our day-to-day job. While Razl is a DB compare tool, Razl has much more usages than most people think it does, so I decided to put a blog post around a couple of things that can be done with Razl.

Automated Content Synchronization between Production and Staging/QA Environments

Many Sitecore developers face the problem that the Staging/QA environments don’t have fresh content. A quality assurance team working with fake/old data, is not ideal and might cause some issues. For example, imagine a client disapproving a working module because it is hard to show how  it will work once it gets to production. This is a situation in which Razl shines. Razl’s APIs allow us to write which can synchronize the Staging/QA environments with fresh content on a daily/hourly basis. The scripts are fairly simple to write and they are even simpler to execute. The scripts can be run with your favorite scheduling mechanism. For more information about Razl Scripting you can check the following articles:

Introducing Razl Scripting (Follow @me_mikeshaw)
Official Razl Documentation About Scripting 
Razl Script Mode (Follow @mikeedwards83)

Setting Up New Development Environments

When adding new people to the team, setting up their Development Environments might be a long and painful process, especially with complex solutions. Razl makes it easy for the new developer to grab all the content and/or some other necessary pieces (custom modules, things from the core database, etc.) and start working.

On Demand or Automated Synchronization between Production and Development Environments

There are some situations in which bugs are caused by the live content and are hard to reproduce on development workboxes. Razl makes it easy to get the pieces that caused issues, bring it to their local environment and now the developers to properly debug them.

Keeping Track on Content Changes

Razl comes with History View that uses the Sitecore History Engine. The history view provides an amazing option to track what has been changed and who has changed it, so when something goes wrong it is easier to check what the cause is and who caused it. For more information about Razl History View, you can check the following articles:

Official Razl Documentation About History View
Razl History View (Follow @mikeedwards83)

Simple Way to Visually Compare CMS/Master Database and CDS/Web Database

Sometimes there might be problems with content publishing from CMS to CDS Servers. Razl makes it easy to track and resolve these kinds of problems. If anything goes wrong with the publishing process, and republish is not an option, Razl can be used to compare the Master and the Web Database. It allows us to check what was left unpublished and helps us identify the problem.

Simple Way to Migrate Content from QA/Staging Content to Production

Sometimes, especially in tight deadline situations, the content is entered on some of the Pre-Production Environments. Here Razl comes in handy to easily move the content between environments.

In conclusion these are just some of the core functionalities that Razl offers in our day-to-day work. It can also be used in other automation scenarios like automatic content synchronization on deploy, moving content across environments via workflow and etc.

Make sure to check out this places for more invaluable Razl Information:

The official Razl Documentation
Hedgehog Development Razl Posts
Mike Edwards Razl Posts

Happy Razling ! 🙂

The Amazing World of RAZL Part 2 – Script Like A Boss

 

Glass Mapper and Custom Time Field

Every Sitecore developer struggled with the problem of implementing a time field and many developers found this article which provides good solution to the problem. The field works well, uses Sitecore built-in timepicker and is easy to implement. The huge problem comes when trying to use the custom time field with one of the most used Sitecore ORMs – Glass Mapper. This arises from the fact that the time picker uses TimeSpan to store its value and Glass doesn’t support TimeSpan values out-of-the-box. Fortunately there are 2 possible solutions to the problem – creating Glass Abstract Data Mapper (in order to support TimeSpan values) or modifying the Custom Time Field (in order for it to work with DateTime which is supported by the default Glass Date Time Handler).

Creating an Abstract Data Mapper Approach

Creating and registering an AbstractSitecoreFieldMapper for Glass is pretty straightforward. The only implementations required are GetFieldValue, SetFieldValue. The other important part is to call the base constructor with Type (or Type array) which is used by Glass to know which types are handled by the custom mapper.

You can find the implementation of the AbstractSitecoreFieldMapper bellow.


namespace Sandbox.GlassExtensions
{
    using Glass.Mapper.Sc;
    using Glass.Mapper.Sc.DataMappers;
    using Sitecore;
    using System;

    public class SitecoreFieldTimeHandler : AbstractSitecoreFieldMapper
    {
        public SitecoreFieldTimeHandler() :
            base(typeof (TimeSpan))
        {

        }

        public override object GetFieldValue(string fieldValue,
            Glass.Mapper.Sc.Configuration.SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            return DateUtil.ParseTimeSpan(fieldValue, TimeSpan.Zero);
        }

        public override string SetFieldValue(object value,
            Glass.Mapper.Sc.Configuration.SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            if (value is TimeSpan)
            {
                TimeSpan time = (TimeSpan)value;
                return time.ToString();
            }

            throw new NotSupportedException("The value is not of type System.TimeSpan");
        }
    }
}

After the handler is implemented it needs to be registered in App_Start/GlassMapperScCustom.cs. Keep in mind this code is only valid if you are using Glass with Castle Windsor (via Glass.Mapper.SC.CastleWindsor). If you are using a custom implementation with different dependency injection framework – adjust the registration.


public static void CastleConfig(IWindsorContainer container)
{
	var config = new Config();

	 container.Register(
		        Component.For<AbstractDataMapper>()
		            .ImplementedBy<SitecoreFieldTimeHandler>()
		            .LifestyleCustom<NoTrackLifestyleManager>());

	container.Install(new SitecoreInstaller(config));
}

And that is it !

If you are not happy with TimeSpans for storing time – feel free to implement you custom data type to store the value.

NOTE: If you are using TDS Code Generation with GlassV3 Templates you will need to update GlassV3Item.tt GetGlassFieldType method. Example of what should be added is shown bellow.

case "[TIMEFIELDNAME]":
   return "TimeSpan";

Modifying the Custom Time Field Approach

This approach uses the default Glass Date Time Handler (The code can be found here) and introduces small modifications to the Custom Time Field.

The modified code can be found bellow.


namespace Sandbox.TimeField
{
    using Sitecore;
    using Sitecore.Data.Items;
    using Sitecore.Diagnostics;
    using Sitecore.Shell.Applications.ContentEditor;
    using Sitecore.Web.UI.HtmlControls;
    using Sitecore.Web.UI.Sheer;
    using System;

    public sealed class GlassTimeField : Input, IContentField
    {
        private TimePicker _picker;

        public string ItemID
        {
            get { return GetViewStateString("ItemID"); }
            set
            {
                Assert.ArgumentNotNull(value, "value");
                SetViewStateString("ItemID", value);
            }
        }

        public string RealValue
        {
            get { return GetViewStateString("RealValue"); }
            set
            {
                Assert.ArgumentNotNull(value, "value");
                SetViewStateString("RealValue", value);
            }
        }

        public GlassTimeField()
        {
            Class = "scContentControl";
            Change = "#";
            Activation = true;
        }

        public override void HandleMessage(Message message)
        {
            Assert.ArgumentNotNull(message, "message");
            base.HandleMessage(message);
            string name;
            if (message["id"] != ID || (name = message.Name) == null)
                return;
            if (name == "contentdate:today")
            {
                Now();
            }
            else
            {
                if (name != "contentdate:clear")
                    return;
                ClearField();
            }
        }

        public string GetValue()
        {
            return
                (System.DateTime.MinValue +
                 DateUtil.ParseTimeSpan(_picker == null ? RealValue : _picker.Value, TimeSpan.Zero))
                    .ToString("yyyyMMddTHHmmss");
        }

        public void SetValue(string value)
        {
            Assert.ArgumentNotNull(value, "value");
            RealValue = value;
            if (_picker == null)
                return;
            _picker.Value = value.Length == 15 ? DateUtil.IsoDateToDateTime(value).ToString("t") : value;
        }

        protected override Item GetItem()
        {
            return Client.ContentDatabase.GetItem(ItemID);
        }

        protected override bool LoadPostData(string value)
        {
            if (!base.LoadPostData(value))
                return false;
            _picker.Value = value ?? string.Empty;
            return true;
        }

        protected override void OnInit(EventArgs e)
        {
            SetViewStateBool("Showtime", true);
            _picker = new TimePicker();
            _picker.ID = ID + "_picker";
            Controls.Add(_picker);
            if (!string.IsNullOrEmpty(RealValue))
                _picker.Value = RealValue.Length == 15
                    ? DateUtil.IsoDateToDateTime(RealValue).ToString("t")
                    : RealValue;
            _picker.OnChanged += (EventHandler) ((param0, param1) => SetModified());
            _picker.Disabled = Disabled;
            base.OnInit(e);
        }

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            ServerProperties["Value"] = ServerProperties["Value"];
            ServerProperties["RealValue"] = ServerProperties["RealValue"];
        }

        protected override void SetModified()
        {
            base.SetModified();
            if (!TrackModified)
                return;

            Sitecore.Context.ClientPage.Modified = true;
        }

        private void ClearField()
        {
            SetRealValue(string.Empty);
        }

        private void SetRealValue(string realvalue)
        {
            if (realvalue != RealValue)
                SetModified();
            RealValue = realvalue;
            _picker.Value = realvalue;
        }

        private void Now()
        {
            SetRealValue(DateUtil.IsoNowTime);
        }
    }
}

The modified methods are GetValue, SetValue and OnInit. The idea is to force the Time Field to work with Sitecore`s IsoDateTimeFormat. A slightly dirty approach – but it does the job.

Happy Glass Mapping ! 🙂