Designing a Page Editor Experience: 1 - Setting up the Code

September 20, 2013

Shout Out

As a shout out, this series started because I was recently speaking with Mark Ursino and he gave me a demonstration of a recent build where he had taken advantage of a few really slick features in Page Editor. I decided to do some research to see what Page Editor could really do. In doing my research I came across a lot of great posts that individually detail pieces of functionality, some of which I'd seen demonstrated at the last Symposium. Others go in depth to explain why certain choices are lend to a better framework. For this post, I wanted to weave together as much of a cohesive picture along with some commentary and lots of referential links for developers to see a fuller Page Editor experience.

Preface

Before I begin, I want to bring to light some things to consider. The Page Editor has evolved to allow you to you store related content in DataSource fields on sublayouts as opposed to storing this information in fields, which is traditionally how many Sitecore sites were built. It's important to understand how the relationships between DataSources are stored in Sitecore and how they're changing between Sitecore 6.6 and 7. In 6.6 the DataSource for a sublayout is stored in the presentation details using the item's path (ie: /sitecore/content/home/pagename). As of 7 this was changed so that the DataSource is stored in the link database as an item ID. This is important for a few different reasons:

  • if you move, rename or remove a data source item, any sublayouts storing the item's path will not be updated, the values will not be easy to cleanup and if you don't code your pages correctly, you could have exceptions being thrown when your page tries to access fields on null objects.
  • as sublayouts are added to the site and items are created as data sources, if a user removes the sublayouts from a page, the items will likely not be removed with them and you'll have to figure out a process for garbage collection on this content.
  • if and when you upgrade from 6.6 to 7, consider that you will want to convert your system from storing the datasources in the presentation details to the link database. I'm not sure if there is any formal way to do this or not yet so do your research.

If you do choose to go ahead with 6.6 and store the DataSource information in the Presentation Details of a page, one thing you can do to help yourself is modify your system so that Sitecore stores the item ID's instead of their paths. This should at least make it somewhat easier to convert and more importantly much less prone to breakage.

Also, if you rely on Lucene search, consider that when you're storing the relationships between a page and it's content in the Presentation Details or link database, you're going to face new challenges to associate this content to a page. With 6.6 you'll need to figure out how to extract the DataSource ID's from the xml in the Layout field and with 7 you'll have to figure out how to gather the information from the link database.

Getting Started

Ok, with that said, whether or not you choose to store content relationships in the presentation details, Sitecore's Page Editor, has a lot to offer. When you get started thinking about designing an experience for Page Editor your journey will start with the code itself. There's a lot of great posts coming out focusing on component based approach including this really thorough series. This push has been for reuse and modularity but also more importantly to support the tools provided by Sitecore for tracking and personalization. What this means is that when you're designing your templates you should do so with consideration towards breaking up the page into reusable components whose content is interchangeable. Components are a general reference to user control sublayouts, xsl renderings and library server controls. I prefer working with sublayouts so going forward my examples will use them but there are other options.

On each of the sublayouts (renderings or server controls), to make the content fields accessible you should be using FieldRenderers. They live in the Sitecore.Web.UI.WebControls namespace and there are several controls built specifically for different field types: Date, Link etc. To learn more about these controls you can bone up a bit by perusing the Presentation Component Reference(System Overview), the Presentation Component API Cookbook(In-Depth Developer Reference) and the Presentation Component Cookbook(Working inside Sitecore)
Otherwise if you need to customize the output you may implement your own by extending the FieldControl class or higher(IE: FieldRenderer).

Once you've wired up your UI, following the path of the sublayout design patterns emerging, you'll want to setup a base class for your sublayouts that provide access to the sublayout parameters and datasource(as opposed to using the Sitecore.Context.Item). Below I've provided a sample base class that provides access to a DataSourceItem, ContextItem and a DataSourceOrContext. DataSourceOrContext will fall back to the ContextItem if the DataSourceItem is null. The reason for this is because by default, FieldRenderers select the Sitecore.Context.Item as their datasource but that might not be preferred. By using this type of base class provides a way for sublayout to specifically decide which source it is targeting by overriding the PreferredDataSource which if untouched, defaults to DataSourceOrContext. This class then recursively sets the datasource for all FieldRenderers on the sublayout. I sourced the logic for this from this great post. As an example, if you change the PreferredDataSource item to use only the DataSourceItem, the recursive function will hide the controls if the DataSourceItem is null so that the FieldRenderer's won't attempt to display data from the Sitecore.Context.Item.

public class BaseSublayout : System.Web.UI.UserControl { 
	private Item _DataSourceItem; 
	public Item DataSourceItem {  
		get {   
			if (_DataSourceItem == null)    
				_DataSourceItem = Sitecore.Context.Database.GetItem(((Sublayout)Parent).DataSource);   
			return _DataSourceItem;  
		}  
		set { _DataSourceItem = value;  } 
	}

	public Item ContextItem {  
		get {   
			return Sitecore.Context.Item; 
		}	
	}

	private Item _DataSourceOrContext;
	public Item DataSourceOrContext {
		get {   
			if (_DataSourceOrContext == null)    
				_DataSourceOrContext = (DataSourceItem != null) ? DataSourceItem : ContextItem;   
			return _DataSourceOrContext;  
		}
	}

	private NameValueCollection _Parameters; 
	public NameValueCollection Parameters {  
		get {   
			if (_Parameters == null)    
				Parameters = Sitecore.Web.WebUtil.ParseUrlParameters(((Sublayout)Parent).Parameters);   
			return _Parameters;  
		}  
		set {   _Parameters = value;  } 
	}

	public virtual Item PreferredDataSource { get { return DataSourceOrContext; } }

	protected void ApplyDataSource() {
		foreach (Control c in Controls)
			RecurseControls(c, PreferredDataSource);
	}

	private void RecurseControls(Control parent, Item dsItem) {
		string typeName = parent.GetType().FullName;
		if (dsItem == null) {
			parent.Visible = false;
		} else if (typeName.StartsWith("Sitecore.Web.UI.WebControls")) {
			if(typeName.EndsWith("FieldRenderer"){
				Sitecore.Web.UI.WebControls.FieldRenderer f = (FieldRenderer)parent;
				f.Item = dsItem;
			} else {
				Sitecore.Web.UI.WebControls.FieldControl f = (FieldControl)parent;
				f.Item = dsItem;
			}
		}

		foreach (Control c in parent.Controls)
			RecurseControls(c, dsItem);
	}

	protected virtual void Page_Load(object sender, EventArgs e) {
		ApplyDataSource();
	}
}

I'm also bootstrapping the recursive function in the Page_Load method so if you want to work within that method on the local instance you'll want to override the method but make sure you call the Page_Load method from the base class:

protected override void Page_Load(object sender, EventArgs e) {
	... your code ...
	base.Page_Load(sender, e);
}