Multi Root Treelist

This is an extension of Kamsar’s Multi Root Treelist(https://kamsar.net/index.php/2015/05/A-Multiple-Root-Treelist-Field/), which also supports using sitecore queries, relative paths and if the datasource does not exist it will not fall back to sitecore/content.

using System;
using System.Linq;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Text;
using Sitecore.Web;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.WebControls;

namespace ChrTech.FieldTypes
{
    public class MultiRootTreeList : TreeList
    {
        protected override void OnLoad(EventArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            base.OnLoad(args);

            if (!Sitecore.Context.ClientPage.IsEvent)
            {
                // find the existing TreeviewEx that the base OnLoad added, get a ref to its parent, and remove it from controls
                var existingTreeView = (TreeviewEx)WebUtil.FindControlOfType(this, typeof(TreeviewEx));
                var treeviewParent = existingTreeView.Parent;

                existingTreeView.Parent.Controls.Clear(); // remove stock treeviewex, we replace with multiroot

                // find the existing DataContext that the base OnLoad added, get a ref to its parent, and remove it from controls
                var dataContext = (DataContext)WebUtil.FindControlOfType(this, typeof(DataContext));
                var dataContextParent = dataContext.Parent;

                dataContextParent.Controls.Remove(dataContext); // remove stock datacontext, we parse our own

                // create our MultiRootTreeview to replace the TreeviewEx
                var impostor = new Sitecore.Web.UI.WebControls.MultiRootTreeview();
                impostor.ID = existingTreeView.ID;
                impostor.DblClick = existingTreeView.DblClick;
                impostor.Enabled = existingTreeView.Enabled;
                impostor.DisplayFieldName = existingTreeView.DisplayFieldName;

                // parse the data source and create appropriate data contexts out of it
                var dataContexts = ParseDataContexts(dataContext);

                impostor.DataContext = string.Join("|", dataContexts.Select(x => x.ID));
                foreach (var context in dataContexts) dataContextParent.Controls.Add(context);

                // inject our replaced control where the TreeviewEx originally was
                treeviewParent.Controls.Add(impostor);
            }
        }

        ///
<summary>
        /// Parses multiple source roots into discrete data context controls (e.g. 'dataSource=/sitecore/content|/sitecore/media library')
        /// </summary>

        /// <param name="originalDataContext">The original data context the base control generated. We reuse some of its property values.</param>
        /// <returns></returns>
        protected virtual DataContext[] ParseDataContexts(DataContext originalDataContext)
        {
            return new ListString(DataSource).Select(x => CreateDataContext(originalDataContext, x)).Where(x => x != null).ToArray();
        }

        ///
<summary>
        /// Creates a DataContext control for a given Sitecore path data source
        /// </summary>

        protected virtual DataContext CreateDataContext(DataContext baseDataContext, string dataSource)
        {
            DataContext dataContext = new DataContext();
            dataContext.ID = GetUniqueID("D");
            dataContext.Filter = baseDataContext.Filter;
            dataContext.DataViewName = "Master";
            if (!string.IsNullOrEmpty(DatabaseName))
            {
                dataContext.Parameters = "databasename=" + DatabaseName;
            }

            var contentItem = Sitecore.Context.ContentDatabase.GetItem(Sitecore.Data.ID.Parse(ItemID));
            // If it is a relative path
            if (dataSource.StartsWith("."))
            {
                if(contentItem != null)
                    dataSource = contentItem.Paths.FullPath + dataSource.Remove(0, 1);
            }
            else if (dataSource.StartsWith("q:"))
            {
                var query = dataSource.Replace("q:", "");
                var itm = contentItem.Parent.Axes.SelectSingleItem(query);

                if (itm != null)
                    dataSource =  itm.Paths.FullPath;
            }

            var rootItem = Sitecore.Context.ContentDatabase.GetItem(dataSource);
            if (rootItem == null)
                return null;

            dataContext.Root = dataSource;
            dataContext.Language = Sitecore.Globalization.Language.Parse(ItemLanguage);

            return dataContext;
        }
    }
}

Structure your data in Sitecore!

How you structure your data in Sitecore is important, not only for the editors using Sitecore, but also for the performance of Sitecore itself. In some of the more severe cases, that I have seen, editors experience item load times of more than 10 seconds.

The problem

Article pages on a site all have tags defined, these tags are all defined in one folder in Sitecore, unstructured.

On the Article page template there is a field of the type Treelist, defined with the data source pointing to the folder with the tags.

If you only have a few tags, say 10 or 20, this is not a big issue, however any site that requires a cms of Sitecore’s caliber will have need of more than 10 or 20 tags and so here lies the problem, every time you load a new Article page item, you will also load however many tag items there are in the tag folder and the load time of the Article page item will increase proportionally with the number of tag items.

The solution

I have found 2 solutions to this problem, there is the quick and dirty one and there is the optimal one.

The quick and dirty solution is simple, you change the field type from TreeList to TreeListEx:

to

The difference between these to is that on TreeList the tag items are loaded every time the item loads, with TreeListEx, it only loads the selected ones and the rest is only when you open the TreeListEx Dialog. This is fine as a temporary fix, but it far from optimal, if we imagine there was no performance problem at all, the editors would still have to sift through an increasing number of tags.

The optimal solution is therefore, as the title says, to structure your data, you might say that if you are spending a lot of time finding the right data in Sitecore, then the system itself will also have to spend a lot of time, relatively. If we take the example with Tags, you might structure the data in one of two ways:

  1. Categorization: Break your tags down into Categories.
  2. Alphabetization: Simply create a folder for Tags that begin with a, b, c and so on.

In both ways you avoid the initial load of an ever increasing number of tags, every time you load an item with a tag field.

 

Sitecore insert media dialog

I recently had a problem with the insert image dialog opened from the rich text editor. Whenever I initially expanded a node in the tree, it would take upwards of 12 seconds to expand the node, no matter how many elements were under the node.

So naturally I first searched a bit around and found nothing of use. So I created a support ticket on the Sitecore support site. I sent them .Net traces and other details.

They thought the most likely problem was the facets under /sitecore/system/Settings/Buckets/Facets

2017-01-23_1658

and after a bit of trial and error I concluded that it were the 2 facets: “Date Range” and “Creation Date & Author” which caused the problems.

Sitecore support also referenced af KB article: https://kb.sitecore.net/articles/576214

The Queen problem

About two years ago I was given an assignment to do for an interview.

The question was how many queens can you place on a chess board without any of them being able to hit each other. Calculate this using code and use OOP principles.

This was a really fun and challenging assignment and the result of my efforts was a project which calculated the result fairly fast and also came with the number of different possibilities of 2, 3, 4, 5… queens.

I went back to look at the project thinking I would spot errors in the project, to my surprise, I did not find any, I did do some minor refactoring and method renaming, but the project was sound and solid.

My conclusion is that you do your best work when both being challenged and having fun.

I had the source code in VS online, now I put it up on GitHub, just in case you want to have a look, but if you haven’t tried to solve the problem yourself, I recommend that you do.

https://github.com/Chrlol/DronningeProblemet

 

Old solutions and implementing new stuff in them.

I recently had to implement a new search provider in an old Sitecore SIP solution.

This is the story of the process.

The requirements demanded that a Web API be implemented to facilitate search, herein lies the first problem, Web API requires .Net 4.0 and since this is an old solution it only ran .Net 3.5. At this point i did not have this assignment and the developer who did tried to port the solution to a newer .Net version, without success.

Then I got the assignment and while briefly discussing the issue with a colleague at the annual Christmas party, I got the idea for the solution. The Web API did not require any information only available in the Sitecore solution, so why not just create the Web API as a separate solution. So I implemented a general Web API with the search features in a way so it could be used by other solutions as well and thus the first problem was solved.

Now this is an Intranet solution, so fast forward through a few setup issues to make sure each user client could reach the Web API.

The solution was working, the users could search the site, however in order to edit content in such an old Sitecore version IE in Compatibility mode is used and here there is no native support for JSON, which the search provider required. So we did a few tricks with meta tags and when editing content you were in Compatibility mode and when just viewing the site you were forced out of Compatibility mode, now the issue was solved, they could both edit, view the page and search, however. When shifting from viewing to editing mode, an error message was displayed, saying that xdomain was not supported in this version, there was no real effect, everything was working, but the error message had to go. I scoured the internet for how to suppress this message, finally I found something, a piece of JavaScript that implemented native support for JSON, so I put the script in the top of my JavaScript file and everything worked.

The script and source: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON

:

if (!window.JSON) {
  window.JSON = {
    parse: function(sJSON) { return eval('(' + sJSON + ')'); },
    stringify: (function () {
      var toString = Object.prototype.toString;
      var isArray = Array.isArray || function (a) { return toString.call(a) === '[object Array]'; };
      var escMap = {'"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t'};
      var escFunc = function (m) { return escMap[m] || '\\u' + (m.charCodeAt(0) + 0x10000).toString(16).substr(1); };
      var escRE = /[\\"\u0000-\u001F\u2028\u2029]/g;
      return function stringify(value) {
        if (value == null) {
          return 'null';
        } else if (typeof value === 'number') {
          return isFinite(value) ? value.toString() : 'null';
        } else if (typeof value === 'boolean') {
          return value.toString();
        } else if (typeof value === 'object') {
          if (typeof value.toJSON === 'function') {
            return stringify(value.toJSON());
          } else if (isArray(value)) {
            var res = '[';
            for (var i = 0; i < value.length; i++)
              res += (i ? ', ' : '') + stringify(value[i]);
            return res + ']';
          } else if (toString.call(value) === '[object Object]') {
            var tmp = [];
            for (var k in value) {
              if (value.hasOwnProperty(k))
                tmp.push(stringify(k) + ': ' + stringify(value[k]));
            }
            return '{' + tmp.join(', ') + '}';
          }
        }
        return '"' + value.toString().replace(escRE, escFunc) + '"';
      };
    })()
  };
}