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 ! 🙂

 

Sitecore Admin Pages Cheat Sheet

It is doubtful that there is a Sitecore Developer who never opened /sitecore/admin/showconfig.aspx at least once in his life to check what went wrong with the amazing configuration patch that hadn`t worked. Showconfig is just one of the many admin pages that Sitecore has by default. Other admin pages are not so popular, so I decided to put a blog post about them.

Cache

URL: http://www.example.com/sitecore/admin/cache.aspx

This page displays details about the Sitecore cache for the instance. There you can find useful information  about the prefetch cache, the item cache and all the other enabled caches (if you are interested in cache optimizations, you can take a look at some of the modules in the Sitecore Marketplace).

DB Browser

URL: http://www.example.com/sitecore/admin/dbbrowser.aspx

A quick way to browse through the separate Sitecore databases. It is pretty lightweight and supports previewing of item’s fields, versions, languages etc. The DB Browser also allows preview of the file system. The most amazing thing about it is the option to delete all of the item’s children – something which is not available from the content editor itself without writing custom code.

Login

URL: http://www.example.com/sitecore/admin/login.aspx

The default admin login form. If you are not authorized to access any of the admin pages you will be redirected here. The good thing about the form is that it supports returnUrl query string parameter which comes in handy when you want to protect some custom sections on the front-end.

Restore

URL: http://www.example.com/sitecore/admin/restore.aspx

I personally never used this, but from the code of the page (or at least the one I got from decompilation of the sitecore.client.dll) it is not doing much 🙂

 protected void ButtonGo_Click(object sender, EventArgs e)
 {
    using (new SecurityDisabler())
      MainUtil.Out("Done.");
 }

Serialization

URL: http://www.example.com/sitecore/admin/serialization.aspx

A quick way to serialize/update/revert databases.Nothing special here, the serialized data will be dropped to the Data folder of the website.

Show Config

URL: http://www.example.com/sitecore/admin/showconfig.aspx

The infamous showconfig page. Shows the compiled Sitecore config. Keep in mind it just shows the <sitecore> node and its descendants. An amazing way to check how the config replacements worked out.

Stats

URL: http://www.example.com/sitecore/admin/stats.aspx

The page provides rendering statistics for all registered sites. Includes load times, cache sizes, etc.

Unlock Admin

URL: http://www.example.com/sitecore/admin/

unlock_admin.aspx

Used to unlock the admin user (if it was locked for some reason). The feature is disabled by default. To enable it, the actual aspx file needs to be edited. There is a boolean value there called enableUnlockButton which needs to be set to true.

Update Installation Wizard

URL: http://www.example.com/sitecore/admin/

UpdateInstallationWizard.aspx

Not to be confused with the package installer. This is the page which is used when upgrading the Sitecore instance, so most of the people might be familiar with it. It allows the users to upload .update packages and execute them over the Sitecore instance. It is also leveraged by some external tools (for example TDS generates .update packages for files and items). There is also a pretty cool open source tool out there, called Sitecore.Ship, which allows us to use the UpdateInstallationWizard by posting files via HTTP.

All the pages covered are available in Sitecore 6.4+. The other admin pages are available only for the listed version and higher.

Starting from Sitecore 6.6

Set Application Center Endpoint

URL: http://www.example.com/sitecore/admin/SetSACEndpoint.aspx

Used to change the Sitecore App Center endpoint. It is useful when playing whit Email Campaign Manager.

Starting From Sitecore 7.0

Fill Database

URL: http://www.example.com/sitecore/admin/

FillDB.aspx

The page was introduced in Sitecore 7.0. It allows the developers to create huge amount of test data. The page needs to be enabled via setting EnableFillDB in /App_Config/Include/Sitecore.Buckets.config. A quick walkthrough on how to use this page will be covered in a separate blog post, as the specifics are not so easy to explain. Meanwhile here is an article from the Sitecore Development Team which explains how to use the page with Sitecore 7.2 –  FillDB Updates.

LINQ Scratchpad

URL: http://www.example.com/sitecore/admin/

LINQScratchpad.aspx

A good place to test your Sitecore queries over the current context. Pretty good testing tool when executing queries over indexes or when you are not using any ORM. For more information about the LINQ Scratchpad you can check this article from the Sitecore Development Team –  Sitecore 7 LinqScratchPad.

Pipeline Profiler

URL: http://www.example.com/sitecore/admin/

Pipelines.aspx

The pipeline profiler allows the developers to profile the performance of the Sitecore Pipelines. You can check the number of executions, execution times, etc. Keep in mind that the Pipeline Profiling is not enabled by default and it needs to be enabled by renaming /App_Config/Include/Sitecore.PipelineProfiling.config.disabled to  /App_Config/Include/Sitecore.PipelineProfiling.config. There is also an option to measure the CPU usage of the pipelines by setting Pipelines.Profiling.MeasureCpuTime to true in the config file. Here is an awesome article by John West which explains how to use the Pipeline Profiler – Sitecore 7 Pipeline Profiling

Remove Broken Links

URL: http://www.example.com/sitecore/admin/RemoveBrokenLinks.aspx

Used to remove all broken links in a database. Nothing quite special about this page, but comes in handy after content reorganizations, content imports and content clean-ups. Comes with an option to auto-serialize the modified items, so they can be easily restored across instances.

Happy admining 🙂 !

Starting From Sitecore 7.5

Media Hash

URL: http://www.example.com/sitecore/admin/mediahash.aspx

As most of you know starting from Sitecore 7.5 the process for the server image resizing was changed, as this process is well described by Sean Holmesby on his blog I will not get into much detail. This page allows to generate the hash values required by Sitecore to return the resized image.

Rebuild Reporting Database

URL: http://www.example.com/sitecore/admin/RebuildReportingDB.aspx

Starting from Sitecore 7.5 Sitecore now uses MongoDB for storing analytics data, but most of the aggregated data is stored in the SQL Reporting Database. In the rare cases of inconsistencies or problems with the processed data – this page can be used for rebuilding the reporting database. Keep in mind that this process requires your instance to have secondary reporting database attached in order to transfer the data. You can find more information about rebuilding the reporting database on the official documentation.

Starting From Sitecore 8.0

Path Analyzer

URL: http://www.example.com/sitecore/admin/PathAnalyzer.aspx

Admin page for Path Analyzer Utilities. Contains the maps manager and if they are deployed correctly. Also used to rebuilding the historic maps, triggering the map agents etc.

Redeploy Marketing Data

URL: http://www.example.com/sitecore/admin/RedeployMarketingData.aspx

Used to redeploy the default Experience Analytics segments. Also has an option to redeploy the Path Analyzer Mappings.

Starting From Sitecore 8.1

Install Language

URL: http://www.example.com/sitecore/admin/InstallLanguage.aspx

Used to install a new language for the Sitecore content. Also has an option to run the Sitecore UI in the selected language, so be careful when using this feature !

Rebuild Key Behavior Cache

URL: http://www.example.com/sitecore/admin/RebuildKeyBehaviorCache.aspx

Used to rebuild the Key Behaviour Cache. You can find more information on the Key Behavior Cache on the official documentation.

Sitecore Modal Dialogs Fix For Chrome 37+

In the newest release of Chrome (37), the showModalDialog function is disabled by default (Chromium Blog). This change will affect all Sitecore instances using version lower than 7.1.

Show Modal Dialog Is not Defined

Fortunately, there are options to workaround the issue.

The first approach is to enable the showModalDialog function via the EnableDeprecatedWebPlatformFeatures policy. For more information on how to set policies you can refer to one of these two articles depending on your organization – Set Chrome policies for devices or Set Chrome policies for users. Keep in mind that this fix needs to be applied on every device.

!!!UPDATE!!!

As it seems the javascript solution with window.open is not working as it is not blocking the execution. The window is actually opened, but the changes won`t be saved because they won`t be passed back to the caller. So the only solution is enabling the deprecated web platform features (or upgrading Sitecore to 7.1+). The big problem is that Mozilla currently deprecated the Window.showModalDialog(), so if they obsolete it in the near future the only option will remain to use IE or upgrade to 7.1+.

The second approach is to replace the showModalDialog function with window.open. This should be done in [Web_Root]\sitecore\shell\Controls\Gecko.js. There are two functions which should be modified.

scBrowser.prototype.prompt

Modify the following line (Line 231 in the JS file):

From

return showModalDialog(“/sitecore/shell/prompt.html”, arguments, features);

To

return window.open(“/sitecore/shell/prompt.html”, arguments, features);

scBrowser.prototype.showModalDialog

Modify the following line (The line number varies for different versions of Sitecore. For Sitecore 7.0 it is Line 350 and for Sitecore 6.6 Line 345):

From

showModalDialog(url, arguments, features);

To

window.open(url, arguments, features);

The big difference is that now the opened windows will not block the GUI, because they are opened in a new browser window and the fabulous dim effect is gone :).

Fixed Modal

Hope this will help you stick to your favorite browser !

Here are some useful links from other sources:

Main SDN Discussion Thread – PSA: showModalDialog disabled by default in Chrome 37

Blog Post By Dheer Rajpoot which covers the chrome policies approach in details – Sitecore Modal Pop-ups are not working in Chrome

Blog Post By Ben McCallum – Sitecore modals and problems in newer browsers

Blog Post By Kamruz Jaman using jQuery UI Dialog – Fix for Dialog Modals not working in Sitecore in Chrome 37+ browsers 

Sitecore KB – Sitecore does not work in Chrome 37 and later

Useful Sitecore Query String Parameters

At the end of the day Sitecore is a web based system and as such uses query string parameters to pass viable data. They are used to switch between display modes, sites, languages etc. I found out that remembering these parameters is a huge speed improvement especially in cases when dealing with multi-language, multi-device or page editor enabled solutions. Here is a list of the Sitecore query string parameters that I are most useful in my day-to-day job.

sc_mode – Used to change the display mode of the website. Can have the following values:

  • normal – normal display – the way the users see the site
  • edit – page editor mode
  • preview – preview mode

Example: http://yoursitename.com?sc_mode=preview

sc_lang – Used to change the website language. Can have any value that is defined under /sitecore/system/languages

Example: http://yoursitename.com?sc_lang=fr-FR

sc_itemid – Used to change the current item.

Example: http://yoursitename.com?sc_itemid={12345678-1234-1234-1234-123456789ABC}

sc_site – Used to change the current website.

Example: http://yoursitename.com?sc_site=mywebsite

sc_device – Used to change the device in which the website is displayed. Can be used with the device name or the device id.

Example: http://yoursitename.com?sc_device=mobile

Example: http://yoursitename.com?sc_device={12345678-1234-1234-1234-123456789ABC}

sc_database – Used to change the database from which the content is displayed.

Example: http://yoursitename.com?sc_database=master

 

And here are some of the debug specific query string parameters.

sc_debug – Used to start/stop the debug mode. Possible values are 0 and 1.

Example: http://yoursitename.com?sc_debug=1

sc_prof – Used to start/stop the Sitecore profiler. Possible values are 0 and 1.

Example: http://yoursitename.com?sc_debug=1&sc_prof=1

sc_trace – Used to start/stop the Sitecore trace. Possible values are 0 and 1.

Example: http://yoursitename.com?sc_debug=1&sc_trace=1

sc_ri – Used to show/hide the rendering information. Possible values are 0 and 1.

Example: http://yoursitename.com?sc_debug=1&sc_ri=1

The default debugger usually starts with all three enabled – http://yoursitename.com?sc_debug=1&sc_prof=1&sc_trace=1&sc_ri=1

 

These are the query string params that I usually use in my day-to-day job. There are probably many more of them in Sitecore that are sitting there, waiting for someone to find them 🙂 !

Sitecore Custom Field Validators

In many cases developers rely on the content editors to input valid content. In reality that never happens. The only way to protect ourselves from receiving invalid data from Sitecore is by using validators. Sitecore has pretty nice “out of the box” validation rules for common scenarios. But common scenarios are much like unicorns and the guys in Sitecore know that. Because of that there is a pretty nice way to add our own custom validators. It can be achieved in 3 easy steps.

The scenario described in this example is the following: There is a single line text field which should contain exactly 16 characters. The first 8 characters must be numbers and the other 8 characters must be English alphabet letters. (Please keep in mind this can be implemented via the built in Sitecore.Data.Validators.FieldValidators.RegexValidator).

Step 1 – Creating A Custom Validator

Create a Custom Validator that inherits from StandardValidator with the following code.

using Sitecore.Data.Validators;
using System.Text.RegularExpressions;

namespace Sandbox.Validators
{
    public class SandboxIDValidator : StandardValidator
    {
        private readonly Regex numbersRegex = new Regex(@"^\d+$");
        private readonly Regex lettersRegexnew = new Regex(@"^[A-Za-z]+$");

        protected override ValidatorResult Evaluate()
        {
            string value = base.GetControlValidationValue();

            if (!string.IsNullOrEmpty(value) && value.Length == 16)
            {
                string firstPart = value.Substring(0, 8);
                string secondPart = value.Substring(8, 8);

                if (numbersRegex.IsMatch(firstPart) && lettersRegexnew.IsMatch(secondPart))
                {
                    return ValidatorResult.Valid;
                }
            }

            base.Text = "Text is not a valid Sandbox ID";

            return base.GetFailedResult(ValidatorResult.Error);
        }

        protected override ValidatorResult GetMaxValidatorResult()
        {
            return base.GetFailedResult(ValidatorResult.Error);
        }

        public override string Name
        {
            get { return "Sandbox ID Validator"; }
        }
    }
}

To create a custom validator, the StandardValidator abstract class should be inerited (which inherits the BaseValidator abstract class). The Name property is self descriptive. The evaluate method gets called when the field needs validation. It determines if the value of the field is valid and and in case it is – it should return ValidatorResult.Valid. Otherwise it should return a validation error of some kind (it might be a suggestion or warning) and error message. GetMaxValidator result is best explained by decompiling the Sitecore.Kernel and looking at the remark:

GetMaxValidatorResult

 

Step 2 – Creating a Sitecore Validation Rule

After the code is ready the validator needs to be added as a Validation Rule in Sitecore. The field validators are located under “/sitecore/system/Settings/Validation Rules/Field Rules/”. The new validation rule for the custom validator should look like this.

Creating_a_custom_validator

Step 3 – Attaching the Validator to a Field

In order to use the validator it should be attached to a field. To attach it navigate to the Template Field in the Template. After that expand the Validation Rules section and add it under the appropriate sections where the validator should appear.AttachToItem

At the end the result should look like this:

ValidatedItem

Happy Validating !

 

Switch Sitecore 7.2 Speak UI Media Browser to the good old Sheer UI Media Browser

I am posting this article as a follow up to the following SDN Topic – Sitecore 7.2: Delete image in media picker in page editor.

After upgrading to Sitecore 7.2 the content editors might complain about the new Speak UI Media Browser, as they would not find it as intuitive as the old one (or maybe they will have a hard time getting used to it). Fortunately there is a way to disable the new Speak Media Browser and switch to the good old Sheer one.

It can be achieved by disabling Speak overrideXmlControls in the Sitecore.Speak.config.

Create a custom configuration with the following code.

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<overrideXmlControls>
			<override xmlControl="Sitecore.Shell.Applications.Media.MediaBrowser" with="/sitecore/client/applications/Dialogs/SelectMediaDialog">
				<patch:delete />
			</override>
		</overrideXmlControls>
	</sitecore>
</configuration>

Keep in mind that the configuration file needs to be bellow the Sitecore.Speak.config in the Include folder otherwise the configuration file won’t work because the original Sitecore.Speak.config overrides won`t be registered (a good name will be z_Sitecore.Speak.Overrides.config).

The effects can be seen on the screenshots bellow.

Before:

Speak UI Media Browser
After:

Sheer UI Media Browser

You can also notice that Sitecore.Speak.config is the configuration file in which the old Sheer UI Dialogs were overridden by the new Speak UI Dialogs (under the overrideDialogs node). If you want to switch to the old Sheer Dialogs this is the place to do it.