Content Testing and Context.Site

A quick blog post about Content Testing feature of Sitecore and its unfriendliness towards Context.Site

I went through a few content testing scenarios recently and one thing really puzzled me: Content Testing dialogs stumble upon Context.Site.

Reference Storefront

If you try to set up a test in the Sitecore Commerce reference storefront and send the page through the workflow, here’s how the variants screenshots will look like:

Test Variants Screenshots all show YSOD

The base controller is using Context.Site for view path resolution:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected string GetRenderingView(string renderingViewName = null)
{
/*
ShopName is a property on the CommerceStorefront object that is represented
by an item at Context.Site.RootPath + Context.Site.StartItem
*/
var shopName = StorefrontManager.CurrentStorefront.ShopName;
// ...
const string RenderingViewPathFormatString = "~/Views/{0}/{1}/{2}.cshtml";
// ...
return string.Format(RenderingViewPathFormatString, shopName, "Shared", renderingViewName);
}

And it won’t find anything in the shell site:

1
2
3
4
5
6
7
8
9
Nested Exception

Exception: System.InvalidOperationException
Message: The view '~/Views/shell/Shared/Structures/TopStructure.cshtml' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/shell/Shared/Structures/TopStructure.cshtml
Source: System.Web.Mvc
at System.Web.Mvc.ViewResult.FindView(ControllerContext context)
at System.Web.Mvc.ViewResultBase.ExecuteResult(ControllerContext context)
...

Habitat

Trying the same workflow with test in Habitat stumbles upon the validation step:

Validation shows error 500

Here the Context.Site is being used for custom dictionary functionality:

1
2
3
4
5
6
7
8
9
10
11
12
private Item GetDictionaryRoot(SiteContext site)
{
var dictionaryPath = site.Properties["dictionaryPath"];
if (dictionaryPath == null)
{
throw new ConfigurationErrorsException("No dictionaryPath was specified on the <site> definition.");
}

// ...

return rootItem;
}

And it also errors out in shell:

1
2
3
4
5
6
7
8
9
Nested Exception

Exception: System.Configuration.ConfigurationErrorsException
Message: 'No dictionaryPath was specified on the <site> definition'.
Source: Sitecore.Foundation.Dictionary
at Sitecore.Foundation.Dictionary.Repositories.DictionaryRepository.GetDictionaryRoot(SiteContext site)
at Sitecore.Foundation.Dictionary.Repositories.DictionaryRepository.Get(SiteContext site)
at Sitecore.Foundation.Dictionary.Repositories.DictionaryRepository.get_Current()
...

Just in case you wondered, Preview.ResolveSite doesn’t help.

Conclusion

Alistair Deneys explained that Content Testing needs to run screenshot generation in context of shell to render unpublished content of different versions.

Content Testing needs to quickly learn how to do everything it needs in the context of the current site while getting everything from the master database.

While you probably shouldn’t use Context.Site for view path resolution - we now have official support for MVC areas, and probably shouldn’t use custom dictionary implementation - here’s my blog post on how to make standard dictionary items editable in Experience Editor, you should be allowed to use Context.Site in your page rendering logic if you need it.

Do Not Remove Unused Blobs On Save

I have not been actively hands-on with Sitecore lately. But once in a while I come across a question that sounds like a good puzzle to roll up my sleeves for, and then I just can’t help it.

Query

One of our engineers posted a question. Their client’s CM instance was running noticabely slow and the users were complaining. They quicky identified the bottleneck with the SQL profiler but the finding puzzled them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
IF EXISTS (SELECT NULL 
FROM [SharedFields] WITH (NOLOCK)
WHERE [SharedFields].[Value] LIKE @blobId)
BEGIN
SELECT 1
END
ELSE IF EXISTS (SELECT NULL
FROM [VersionedFields] WITH (NOLOCK)
WHERE [VersionedFields].[Value] LIKE @blobId)
BEGIN
SELECT 1
END
ELSE IF EXISTS (SELECT NULL
FROM [ArchivedFields] WITH (NOLOCK)
WHERE [ArchivedFields].[Value] LIKE @blobId)
BEGIN
SELECT 1
END
ELSE IF EXISTS (SELECT NULL
FROM [UnversionedFields] WITH (NOLOCK)
WHERE [UnversionedFields].[Value] LIKE @blobId)
BEGIN
SELECT 1
END

Who Are You

I have once traversed basic item APIs all the way down to data providers and back so I just knew where to look. SqlServerDataProvider in Sitecore.Kernel has a method with a very telling name that runs this query.

The name of the method is - GetCheckIfBlobShouldBeDeletedSql(). Walking up the usages chain I found who runs it:

1
2
3
4
5
6
7
8
9
10
public override bool SaveItem(...)
{
// ...
if (Settings.RemoveUnusedBlobsOnSave)
{
ManagedThreadPool.QueueUserWorkItem((state => this.RemoveOldBlobs(changes, context)));
}
// ...
}

Every item save will call RemoveOldBlobs() that will end up running the mentioned SQL query if RemoveUnusedBlobsOnSave is set to true.

The method runs asynchronously so it doesn’t directly impact the executing thread, but it does put pressure onto the SQL server. Running LIKE logic looking for GUIDs (even without %) in a non-indexed nvarchar field across mutliple tables will take some cycles.

Recommendation

It’s good that this logic is protected with a feature toggle.

I suggested that the team turns off Settings.RemoveUnusedBlobsOnSave and contacts Sitecore Support.

This behavior was observed in 8.1 Update 2. I opened 8.0 Initial Release just out of curiosity and SaveItem() doesn’t go looking for old BLOBs. I didn’t go through more recent releases but it has got to be a relatively new addition. Probably added for a reason.


If we turn off running it on every item save, when should we run it? Maybe it’s missing the ID of the saved item in the WHERE to make it a lot more specific? Don’t know. I will update this post if/when we hear back from the support team.

A Missing Field Type - Part 2

In part 1 of this series I made an argument that Sitecore needs a new field type that would support workflow and versioning without adding language variance. Let’s see why.

Presentation

Presentation details were Shared in older versions of Sitecore . It means no versioning, no workflow support, and no language variance. A page item under workflow will not publish its latest version until the workflow reaches the final state. Too bad for the Shared fields though. Once modified, the change will be picked up by the smart or full publish. A threat is very much real - just like I hope you are using workflows, I also hope that you are using scheduled publishing agents and don’t let your authors work as admins.

Versioned Layouts

Sitecore 8 introduced versioned layouts. Presentation details can now be both Shared and Final and the end result is merged at run time. The promise of versioned layouts is to workflow-enable the presentation details and to also allow language variance. The problem is - you can’t get one without the other.

One Language

I stand corrected. You actually can have workflow support without language variance. Just don’t translate your content. If your site only supports one language, you’ll enjoy using versioned layouts. I would even suggest that you forego the good old __Renderings (Shared layout) altogether to ensure proper versioning and workflow support of your presentation. Rejoice!

Multilingual

There are more than one way to build a multilingual site in Sitecore. Previously, if your layout was the same across languages you could safely translate a single content tree. Now you can do even more with a single content tree thanks to version layouts that can easily accommodate certain language variances. If the content varies significantly and translated sites feel more like distinct online properties, you will probably build parallel content structures but let’s focus on a more common example.

Single content tree. Translated. Under a workflow. Scheduled publishing agents. Best practices. Right?

A change to the Shared portion of the layout is susceptible to the same accidental publishing as the entire layout in older version of Sitecore.

Understood. Can we use final layouts?

A change to the Versioned portion of the layout, while workflow controlled and versioned, only affects a single language.

Wait a minute. Can I or can I not safely and soundly workflow control my layout?

The answer is - it depends. If your presentation details are exactly the same across all translations, then you probably can. You will need language fallback and a little discipline. Language fallback will propagate the value from one language to another provided that the value in another language is null. An empty layout is not null (try resetting it and look at the raw value if you wonder what it is) so there goes the first wrinkle. Any accidental (or not) change to the layout in Experience Editor done not in context of the primary language will break the fallback chain.

Tough. And what if you have a layout variance in a given language?

The Missing Field Type

Well, like I said, Sitecore needs another field type - Workflowed. And I wouldn’t worry about migration issues to be honest. One of the recent updates changed the way clones are handled. A breaking change indeed. Migration instructions included a simple SQL script to upmigrate all clones. Easy, no big deal. Same could be done to legacy __Renderings if field type changed from Shared to a new Workflowed.

Call your congressman.


Something occurred to me while I was writing this small series. There is another best practice that has a very complicated relationship with the workflow. We embrace it and bet our content architectures on it and yet it gets in a way of a smooth and predictable editorial process. Datasorces. Why is it? Can something be done about it?

Next time on this blog. Stay tuned!

p.s. You can also account for language variance in a layout with personalization by language but I would probably advise against it. Assuming, of course, that you are using marketing automation capabilities of Sitecore and specifically the A/B content testing. Algorithms that generate multivariate permutations and track test performance can’t tell the difference between a functional personalization and a marketing-driven experience variance. Hopefully I get to write about it some time later.

A Missing Field Type - Part 1

If you haven’t read my post from last year about Sitecore’s Items, Fields, Versions, and Languages I would recommend that you do it first. In this post i will make an argument that Shared, Unversioned, and Versioned is not enough and we need another field type - Workflowed. Even if only for one particular field.

Shared

Shared fields are very basic. Value of a shared field has no language variance and no history. It cannot be workflow controlled and it cannot be restored to a previous value. You probably rarely use them on content items managed by the authoring teams exactly for the reasons outlined. You will find them, however, on the Standard Template and throughout the Sitecore internals in general.

Things that are inherently global and stable, things that an item either has or doesn’t have, things that never vary across translations, things that don’t change how an item is rendered - these are the qualities of a shared field. Good examples - __Is Bucket and __Enforce version presence. Bad example - __Renderings.

Unversioned

Unversioned fields are rare these days. Value of an unversioned field will differ across translations but it still has no history and no workflow support. Same qualities as Shared basically plus a per-language variance. An attribute of an item has to be textual or otherwise different per language and yet be global and stable to warrant an unversioned field. Good example - __Short description from the Help section.

Versioned

The bread and butter of content items. Supports language translation, has history, and is subject to a workflow. What’s not to like? The majority of the fields you create should be versioned. Well, at least they should have history and workflow support and what other choice you have then, right?

Having both language and version angles can sometimes produce friction. You can have former without the latter (the Unversioned fields) but not the other way around. If you need history and workflow - and you do almost always need it - you have to also deal with the fact that each language translation has its own value.

Workflowed

I feel that we need one more field type. Unversioned fields add the language (translation) dimension to the Shared fields. Versioned fields add the version (history and workflow) dimension to the Unversioned fields. What if I wanted history and workflow support without language variance? What if there was a Workflowed field type that was subject to a workflow and supported history rollbacks but didn’t have the language angle? Basically, a version dimension added to the Shared fields.

A field without a language angle represents something that is the same across all translations. If you use Shared field type you subject yourself to accidental publishing of new values while everything versioned is still in the draft state. If you use Versioned field type you have to maintain the same value for all translations contract.

Why is it a problem? Next time on this blog. Stay tuned for Part 2.

Sitecore 8.1. Developer's Notes

I took a two months long hiatus from writing to transition into my new role at work. It’s all good now and I am back with an overdue post about Sitecore 8.1

Routes and the MVC Areas

Sitecore 8.1 has finally added native support for MVC areas. If you’ve used areas before (using this option, for example) you probably were calling AreaRegistration.RegisterAllAreas() during <initialize> and registering your routes in RegisterArea():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Web.Mvc;

namespace Solution.Web.Areas.Site
{
public class SiteAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Site";
}
}

public override void RegisterArea(AreaRegistrationContext context)
{
// Register your MVC routes here
}
}
}

You can now take your own area registration out. Or can you? Here’s how Sitecore changed the InitializeRoutes:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected virtual void RegisterRoutes(RouteCollection routes, PipelineArgs args)
{
RouteCollectionExtensions.MapRoute(routes, MvcSettings.SitecoreRouteName ... );
this.SetDefaultValues(routes, args);

// <-- Pay attention to the order
this.SetRouteHandlers(routes, args);

// <-- This is new in 8.1
this.RegisterAreas();

InitializeRoutes.AddRenderersViewFolderToRazorViewEngine();
}

The problem is - area registration is called after route handlers have been set. I briefly mentioned the custom route handlers in my older blog post about Sitecore MVC routing mechanisms. This is what makes otherwise regular MVC routes Sitecore aware. No custom handlers on your routes means no mvc.* pipelines, no PageContext, no analytics.

All the routes that you were registering in RegisterArea() are now not part of the Sitecore MVC ceremonies. They are late to the party.

Read more

Digest of my Sitecore blogs

Before I launched this blog I was actively writing on www.jockstothecore.com and before that on pveller.blogspot.com

Heres’s a collection of everything I have pubished on JocksToTheCore in the last two years:

My Favorites

Sitecore 8 Versioned Layouts

Dynamic Product Details Pages

Sitecore 8 Experience Editor

To The Controller And Back

Web Forms For Marketers

xDB

Custom Field Types

TDS

Assorted