Sunday, February 28, 2016

Sitecore Decennial Series #1 - Know your item, remember your context

Understand the basics

The most common problems I find when working with (other people's) Sitecore solutions, has a root cause in either a lack of understanding of the basic concepts of Sitecore and/or a misunderstanding of same. I don't actually know if this is surprising or to be expected - it is what it is.

I'll try and isolate some of the most common basic mistakes, in no significant order. This is also to say, I consider them ALL equally significant ;-)

1. Understand your Item

Yes I'm talking about you, Sitecore.Data.Item.

Chances are, Item is one of the very first concepts you come across when you start developing your first solution. It might look something like this:

    Item home = Sitecore.Context.Database.GetItem("/sitecore/content/home");
    string headline = home["Headline"];

Simple, yea?  Not so. Reading the syntax like this has a high risk of tricking your mind into thinking a lot of things, all of which are false and will lead you astray sooner or later.

1a. A context is implied

As with the very large majority of all interaction with the Sitecore API, a context is required for any interaction. I will dig into this in detail a bit further on.

That method call, is just a method overload for a call that looks like this:

    Item home = Sitecore.Context.Database.GetItem("/sitecore/content/home", 
                Language.Current, 
                Version.Latest);

Why is this significant?  It's significant because here, as in most API calls, Sitecore breaks out and pulls additional information required to execute the call; in this case it needs to determine what Language Version of the Item to get. And once it knows the language, it needs to know which Version of the Language Version to get.

And while this may not seem obvious to you while still learning the robes of Sitecore development (it didn't, to me), this is actually very significant.

Very many things in the Sitecore API requires some sort of context. And knowing them will be important to you, when you advance into more advanced development.

1b. People say "Item" when they really mean "Item Version".

There really is no such thing as an "Item". Item is a construct in Sitecore that holds a lot of information related to the content of the Item Versions - but really holds none of the actual content. (For the advanced readers; I realise we could argue the merits of this - but as a general principle, this holds true).

So what is on Item?    Important things. Like:
  • Name (by default, defines the URL string for the item)
  • Security
  • Template (could also be called the "Schema" for the Item Versions)
  • Statistics (Last Updated, Updated By, etc.)
  • Publishing Information
  • Workflow Information
  • Validation Rules
But for most intents and purposes, not things you need in your day to day life of creating Accordion components or whatever your task.

Sitecore.Data.ID uniquely identifies any Item in a Sitecore solution. And from this, we now also see, that Sitecore.Data.ID does not adequately represent an Item Version.

1c. Understand the different identifiers. ID is not always what you need.

Unbeknownst to many, judging from rarely I find these in use in Sitecore solutions I look at, Sitecore actually has many better options than Sitecore.Data.ID available. All in the Sitecore.Data namespace.

Given this piece of hackety webforms code (had to destroy the BR tags to keep Blogger happy):

  var home = Sitecore.Context.Database.GetItem("/sitecore/content/home");
  litOutput.Text += $"ID (home.ID): {home.ID}$br />";
  litOutput.Text += $"Uri (home.Uri): {home.Uri}$br />";
  litOutput.Text += $"DataUri: {new DataUri(home.ID, home.Language, home.Version)}$br />";
  litOutput.Text += $"ItemUri: {new ItemUri(home.ID, home.Language, home.Version, home.Database)}$br />";
  litOutput.Text += $"VersionUri: {new VersionUri(home.Language, home.Version)}$br />";

The output is:

  ID (home.ID): {DAC24EDD-44FB-42EF-9ECD-1E8DAF706386}
  Uri (home.Uri): sitecore://master/{DAC24EDD-44FB-42EF-9ECD-1E8DAF706386}?lang=en&ver=1
  DataUri: sitecore://{DAC24EDD-44FB-42EF-9ECD-1E8DAF706386}?lang=en&ver=1
  ItemUri: sitecore://master/{DAC24EDD-44FB-42EF-9ECD-1E8DAF706386}?lang=en&ver=1
  VersionUri: en, 1

Any Item you have instantiated (like from a GetItem() API call) will be uniquely identified by an ItemUri, as found on the .Uri property. .ID tells you only the ID of the underlying Item.

From this we also learn, that the Item we get from the API does not exist outside of a Sitecore Context. With both Database, Language and Version information. This is undoubtedly become a pet peevee for you at one point or another, if you start looking to do Unit Testing or any kind of abstractions to the Sitecore API. My honest advice; leave this be for now. For at least a couple of years into your Sitecore learning curve.

Keep your Item identifiers in mind. Don't use .ID as a cache key when publishing Item Versions. Do use DataUri, ItemUri and VersionUri as appropriate, don't re-invent the wheel with your own bespoke implementations or - worse - just ignore the fact that ID does not tell you all you need to know.

Speaking of pet peevees, here's one of mine.

2. Understand your context

2a. Don't break context or get a context you don't require

Given the following code:

    public Item[] GetNewsInCategory(Item categoryItem)
    {
        List articles = new List();

        if (!string.IsNullOrEmpty(categoryItem["Articles"]))
        {
            foreach (var articleId in categoryItem["Articles"].Split("|".ToCharArray()))
            {
                articles.Add(Sitecore.Context.Database.GetItem(articleId));
            }
        }

        return articles.ToArray();
    }

Actually there are 2 pet peevees of mine in here. One is not using the Sitecore API to properly deal with the MultilistField. The other is the breakout to Sitecore.Context.Database to get the items defined in the field. Why? We already take an Item as a parameter, so we already HAVE a Language and a Database context. At the very LEAST, do this:

    articles.Add(categoryItem.Database.GetItem(articleId));

In the inner loop. If you make methods and these happen to take an Item as an argument, by all means USE the context of that argument to carry on. You'll be happy you did, as you will one day find yourself wanting to call your code from say an Index Handler, an Item Saving event or whatnot - and you cannot assume you have your normal page context available in these cases. I've seen what happens on this particular road to hell, and it usually ends up with a line of code like this getting injected.

    database myDb = Factory.GetDatabase("master");

To try and solve the problem, with Sitecore.Context.Database being NULL in some cases. No, no, no, nope, please, just don't. Use the .Database property of the Item you're dealing with. Whoever instantiated it, already made a context for it (see above; no Item with no context).

That said; what the above code SHOULD look like this this. (Leaving out the argument about argument assertion for now).

    public Item[] GetNewsInCategory(Item categoryItem)
    {
        MultilistField articlesField = categoryItem.Fields["Articles"];
        if (articlesField != null)
            return articlesField.GetItems();
        return new Item[] {};
    }

2b. Context, context, context everywhere

I think you realise by now, I find the subject of Context in Sitecore very important ;-)

Here's the thing. Try and avoid using it. While it is indeed very convenient to just jump out and grab a Sitecore.Context.Site whenever you need it, or Sitecore.Context.Language or whatever it may be. But it is also very bad form for your code. It is in fact an anti-pattern.

"But all of Sitecore is written like this?"

CAREFUL!  PERSONAL OPINION WITH SOME SPECULATION FOLLOWS!

Yes. I don't know what to tell you. I'm pretty sure if the original development team was to start today, much of this codebase would have been done following a different mindset. But most of the codebase you're looking at is between 10 and 15 years old, and Sitecore has always been adamant that backwards compatibility be preserved unless there were very good reasons to break it. They follow principles followed by Microsoft very closely when it comes to this.

A good example of this is my own CorePoint.DomainObjects, one of the very first public ORM mappers for Sitecore. Written almost 8 years ago, and it can still be built on recent Sitecore versions without too much headache.

Sitecore uses static constructs all over the place. It will drive you nuts if you try and code to modern standards, e.g. using Dependency Injection (you should), but that's just how it is. I tell you another thing though; calling through a layered API of static methods and classes is faster than dynamically resolving types at runtime. While we accept this cost today, performance was a much different beast 10 to 15 years ago.

So anyway. Back to my original point. Forgive me for pointing out the obvious here. YOU'RE NOT WRITING A CMS SYSTEM. What you're doing, is writing a codebase that will eventually turn out to be an excellent Sitecore solution, running the website of your (or your client's/employer's) dreams. Nowhere is it stated, your code standards need to follow those of Sitecore. Yes, you need to adhere to Sitecore Best Practices in your interactions with the Sitecore API and all that, obviously, but nothing else in Sitecore dictates how you should organise your project and solution.

And yes, this is a two edged sword, and why initiatives like the Habitat solution surfaces. Complete freedom, unfortunately, also means you have complete freedom to mess up things. Badly.

To bring some concrete suggestions out; if you need a Database in your method or class, ask for it in the constructor or as a parameter. If you need a SiteContext, ask for it. Don't - please don't - try and configure a full Dependency Injection setup and abstract all of Sitecore into interfaces if this is your first Sitecore solution. It will cost you a LOT of time, much much more than you realise, and chances are no one will ever make back that investment of time in your first solutions lifetime. 

Yes, I really did just write that ;-)  Take my word for it. 

2c. Are you sure you need that event handler? And if you do, are you sure you're hooked into the right one?

Look, I'm pretty sure you don't need that item:Saved handler. Why?  Because the real need for them is so very rare. I can probably count on one hand, how many times I've needed to implement one over the course of 10 years of Sitecore development.

Chances are, you're trying to make Sitecore do something it shouldn't really do. This is actually a reference I wrote a blog post about years ago; Just because you can, doesn't mean you should. I'm going to rewrite this as part of this Decennial series, but for now the original post will have to do.

Take a step back; consider if what you're doing is really trying to solve a user training problem with a programming solution. Still need that handler?  Ok then.

Consider the context of your handler then. I often see examples, like an item:Saved handler that manipulates other items or possibly creates and re-creates parts of the Sitecore content tree, all based on a particular field value or something similar. Are you aware that item:Saved is fired as part of the PublishItem process?  (like when the published item is Saved to the "web" Database). 

Check your context, filter your context.

If you do implement handlers and processors, at least make sure they only execute when you expect them to. Check if item.Database.Name really matches the ContentDatabase, abort if it doesn't. Check that your bespoke ItemResolver code is currently serving content for the website you expect. "publishing", "shell", "scheduler" etc. are all websites on your solution, are you aware of that?

Consider these things, whenever you hook into anything. Be it item events, request processors, link managers or otherwise. 

As an example, look at the item:Saved handler that deal with keeping your LinkDatabase updated. (a hugely underestimated resource when it comes to Sitecore development, but this will be a subject for one of my next posts in this series).

    protected void OnItemSaved(object sender, EventArgs args)
    {
      if (args == null || LinkDisabler.IsActive || !Settings.LinkDatabase.UpdateDuringPublish && PublishHelper.IsPublishing())
        return;
      Item obj = Event.ExtractParameter(args, 0) as Item;
      Assert.IsNotNull((object) obj, "No item in parameters");
      LinkDatabase linkDatabase = ItemEventHandler.LinkDatabase;
      if (linkDatabase == null)
        return;
      linkDatabase.UpdateItemVersionReferences(obj);
    }

Notice how, the first statements in the event handler actually deals with asserting, if it should run at all. Sitecore makes no such determination for you, it is YOUR responsibility to ensure this.

This also goes for your PageMode.

2d. What is your current PageMode. Is it relevant?

Considering the current PageMode becomes important, when you're making run-time decisions that could affect the user experience.



Let's say you're putting in some code, to prevent your component from failing if it has been configured with a faulty Datasource. Or alternatively if you want to explicitly throw an Exception in that case, to help your fellow developers track down a bug.

Be careful. At site run-time (when PageMode.IsNormal) it could indeed be considered an error condition, if a component is configured with a Datasource that does not exist. This is quite likely NOT true for many of the other PageModes. Consider this:

An Editor is Page Editing (or Experience Editing, the new bling expression) inserts your component onto a page. What happens (simplified) is, that Sitecore adds your component to a placeholder key and renders it. You may or may not have a Datasource defined at this stage. Don't blow up. Don't YSOD. Your component is in a staging state, and your code needs to consider this. This is what PageMode is for. 

In general, I find it really bad form to YSOD on these specific conditions; like a missing Datasource or a reference field pointing to items that do not exist. Why?  Because it's very likely just User Error. An Editor forgetting to publish a related item (something that is VERY easy to do in Sitecore, even if the current publishing tools make this slightly easier). You really DON'T want to teach your users, if they make a mistake you're going to YSOD their site. You really don't.

Alternatively, if you can, discuss with your users what should happen. Either the component outputs some harmless content to alert them of this condition, or perhaps it hides itself completely. Again, only do this if PageMode.IsNormal or PageMode.IsPreview. Or maybe PageMode.IsDebugging, if you're using the Sitecore Debugger (you should). But consider it, don't just ignore it.



So I think that's it. For this post, anyway. Until next time :-)


3 comments:

VisionsInCode said...
This comment has been removed by the author.
VisionsInCode said...

Great post, I'm already looking forward to the next post in this series :-)

Anonymous said...
This comment has been removed by a blog administrator.