Have you ever said to yourself when looking at base templates of a template in its Content tab “wouldn’t it be great if I could easily navigate to one of these?”
I have had this thought more than once despite having the ability to do this in a template’s Inheritance tab — you can do this by clicking one of the base template links listed:
For some reason I sometimes forget you have the ability to get to a base template of a template in the Inheritance tab — why I forget is no doubt a larger issue I should try to tackle, albeit I’ll leave that for another day — and decided to build something that will be more difficult for me to forget: launching a dialog via a new item context menu option, and selecting one of the base templates of a template in that dialog.
I decided to atomize functionality in my solution by building custom pipelines/processors wherever I felt doing so made sense.
I started off by building a custom pipeline that gets base templates for a template, and defined a data transfer object (DTO) class for it:
using System.Collections.Generic; using Sitecore.Data.Items; using Sitecore.Pipelines; using Sitecore.Web.UI.Sheer; namespace Sitecore.Sandbox.Shell.Framework.Pipelines { public class GetBaseTemplatesArgs : PipelineArgs { public TemplateItem TemplateItem { get; set; } public bool IncludeAncestorBaseTemplates { get; set; } private List<TemplateItem> _BaseTemplates; public List<TemplateItem> BaseTemplates { get { if (_BaseTemplates == null) { _BaseTemplates = new List<TemplateItem>(); } return _BaseTemplates; } set { _BaseTemplates = value; } } } }
Client code must supply the template item that will be used as the starting point for gathering base templates, and can request all ancestor base templates — excluding the Standard Template as you will see below — by setting the IncludeAncestorBaseTemplates property to true.
I then created a class with a Process method that will serve as the only pipeline processor for my new pipeline:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Data.Items; using Sitecore.Diagnostics; namespace Sitecore.Sandbox.Shell.Framework.Pipelines { public class GetBaseTemplates { public void Process(GetBaseTemplatesArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.TemplateItem, "args.TemplateItem"); List<TemplateItem> baseTemplates = new List<TemplateItem>(); GatherBaseTemplateItems(baseTemplates, args.TemplateItem, args.IncludeAncestorBaseTemplates); args.BaseTemplates = baseTemplates; } private static void GatherBaseTemplateItems(List<TemplateItem> baseTemplates, TemplateItem templateItem, bool includeAncestors) { if (includeAncestors) { foreach (TemplateItem baseTemplateItem in templateItem.BaseTemplates) { GatherBaseTemplateItems(baseTemplates, baseTemplateItem, includeAncestors); } } if (!IsStandardTemplate(templateItem) && templateItem.BaseTemplates != null && templateItem.BaseTemplates.Any()) { baseTemplates.AddRange(GetBaseTemplatesExcludeStandardTemplate(templateItem.BaseTemplates)); } } private static IEnumerable<TemplateItem> GetBaseTemplatesExcludeStandardTemplate(TemplateItem templateItem) { if (templateItem == null) { return new List<TemplateItem>(); } return GetBaseTemplatesExcludeStandardTemplate(templateItem.BaseTemplates); } private static IEnumerable<TemplateItem> GetBaseTemplatesExcludeStandardTemplate(IEnumerable<TemplateItem> baseTemplates) { if (baseTemplates != null && baseTemplates.Any()) { return baseTemplates.Where(baseTemplate => !IsStandardTemplate(baseTemplate)); } return baseTemplates; } private static bool IsStandardTemplate(TemplateItem templateItem) { return templateItem.ID == TemplateIDs.StandardTemplate; } } }
Methods in the above class add base templates to a list when the templates are not the Standard Template — I thought it would be a rare occurrence for one to navigate to it, and decided not to include it in the collection.
Further, the method that gathers base templates is recursively executed when client code requests all ancestor base templates be include in the collection.
The next thing I built was functionality to prompt the user for a base template via a dialog, and track which base template was chosen. I decided to do this using a custom client processor, and built the following DTO for it:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Web.UI.Sheer; using Sitecore.Data.Items; namespace Sitecore.Sandbox.Shell.Framework.Pipelines { public class GotoBaseTemplateArgs : ClientPipelineArgs { public TemplateItem TemplateItem { get; set; } public string SelectedBaseTemplateId { get; set; } } }
Just like the other DTO defined above, client code must suppy a template item. The SelectedBaseTemplateId property is set after a user selects a base template in the modal launched by the following class:
using System.Collections.Generic; using System.Linq; using Sitecore.Data.Items; using Sitecore.Data.Managers; using Sitecore.Diagnostics; using Sitecore.Pipelines; using Sitecore.Shell.Applications.Dialogs.ItemLister; using Sitecore.Web.UI.Sheer; namespace Sitecore.Sandbox.Shell.Framework.Pipelines { public class GotoBaseTemplate { public string SelectTemplateButtonText { get; set; } public string ModalIcon { get; set; } public string ModalTitle { get; set; } public string ModalInstructions { get; set; } public void SelectBaseTemplate(GotoBaseTemplateArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.TemplateItem, "args.TemplateItem"); Assert.ArgumentNotNullOrEmpty(SelectTemplateButtonText, "SelectTemplateButtonText"); Assert.ArgumentNotNullOrEmpty(ModalIcon, "ModalIcon"); Assert.ArgumentNotNullOrEmpty(ModalTitle, "ModalTitle"); Assert.ArgumentNotNullOrEmpty(ModalInstructions, "ModalInstructions"); if (!args.IsPostBack) { ItemListerOptions itemListerOptions = new ItemListerOptions { ButtonText = SelectTemplateButtonText, Icon = ModalIcon, Title = ModalTitle, Text = ModalInstructions }; itemListerOptions.Items = GetBaseTemplateItemsForSelection(args.TemplateItem).Select(template => template.InnerItem).ToList(); itemListerOptions.AddTemplate(TemplateIDs.Template); SheerResponse.ShowModalDialog(itemListerOptions.ToUrlString().ToString(), true); args.WaitForPostBack(); } else if (args.HasResult) { args.SelectedBaseTemplateId = args.Result; args.IsPostBack = false; } else { args.AbortPipeline(); } } private IEnumerable<TemplateItem> GetBaseTemplateItemsForSelection(TemplateItem templateItem) { GetBaseTemplatesArgs args = new GetBaseTemplatesArgs { TemplateItem = templateItem, IncludeAncestorBaseTemplates = true, }; CorePipeline.Run("getBaseTemplates", args); return args.BaseTemplates; } public void Execute(GotoBaseTemplateArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNullOrEmpty(args.SelectedBaseTemplateId, "args.SelectedBaseTemplateId"); Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", args.SelectedBaseTemplateId), 1); } } }
The SelectBaseTemplate method above gives the user a list of base templates to choose from — this includes all ancestor base templates of a template minus the Standard Template.
The title, icon, helper text of the modal are supplied via the processor’s xml node in its configuration file — you’ll see this later on in this post.
Once a base template is chosen, its Id is then set in the SelectedBaseTemplateId property of the GotoBaseTemplateArgs instance.
The Execute method brings the user to the selected base template item in the Sitecore content tree.
Now we need a way to launch the code above.
I did this using a custom command that will be wired up to the item context menu:
using System.Collections.Generic; using System.Linq; using Sitecore.Data.Items; using Sitecore.Data.Managers; using Sitecore.Diagnostics; using Sitecore.Shell.Framework.Commands; using Sitecore.Sandbox.Shell.Framework.Pipelines; using Sitecore.Web.UI.Sheer; using Sitecore.Pipelines; namespace Sitecore.Sandbox.Commands { public class GotoBaseTemplateCommand : Command { public override void Execute(CommandContext context) { Context.ClientPage.Start("gotoBaseTemplate", new GotoBaseTemplateArgs { TemplateItem = GetItem(context) }); } public override CommandState QueryState(CommandContext context) { if (ShouldEnable(GetItem(context))) { return CommandState.Enabled; } return CommandState.Hidden; } private static bool ShouldEnable(Item item) { return item != null && IsTemplate(item) && GetBaseTemplates(item).Any(); } private static Item GetItem(CommandContext context) { Assert.ArgumentNotNull(context, "context"); Assert.ArgumentNotNull(context.Items, "context.Items"); return context.Items.FirstOrDefault(); } private static bool IsTemplate(Item item) { Assert.ArgumentNotNull(item, "item"); return TemplateManager.IsTemplate(item); } private static IEnumerable<TemplateItem> GetBaseTemplates(TemplateItem templateItem) { Assert.ArgumentNotNull(templateItem, "templateItem"); GetBaseTemplatesArgs args = new GetBaseTemplatesArgs { TemplateItem = templateItem, IncludeAncestorBaseTemplates = false }; CorePipeline.Run("getBaseTemplates", args); return args.BaseTemplates; } } }
The command above is visible only when the item is a template, and has base templates on it — we invoke the custom pipeline built above to get base templates.
When the command is invoked, we call our custom client processor to prompt the user for a base template to go to.
I then glued everything together using the following configuration file:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <sitecore> <commands> <command name="item:GotoBaseTemplate" type="Sitecore.Sandbox.Commands.GotoBaseTemplateCommand, Sitecore.Sandbox"/> </commands> <pipelines> <getBaseTemplates> <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.GetBaseTemplates, Sitecore.Sandbox"/> </getBaseTemplates> </pipelines> <processors> <gotoBaseTemplate> <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.GotoBaseTemplate, Sitecore.Sandbox" method="SelectBaseTemplate"> <SelectTemplateButtonText>OK</SelectTemplateButtonText> <ModalIcon>Applications/32x32/nav_up_right_blue.png</ModalIcon> <ModalTitle>Select A Base Template</ModalTitle> <ModalInstructions>Select the base template you want to navigate to.</ModalInstructions> </processor> <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.GotoBaseTemplate, Sitecore.Sandbox" method="Execute"/> </gotoBaseTemplate> </processors> </sitecore> </configuration>
I’ve left out how I’ve added the command shown above to the item context menu in the core database. For more information on adding to the item context menu, please see part one and part two of my post showing how to do this.
Let’s see how we did.
I first created some templates for testing. The following template named ‘Meta’ uses two other test templates as base templates:
I also created a ‘Base Page’ template which uses the ‘Meta’ template above:
Next I created ‘The Coolest Page Template Ever’ template — this uses the ‘Base Page’ template as its base template:
I then right-clicked on ‘The Coolest Page Template Ever’ template to launch its context menu, and selected our new menu option:
I was then presented with a dialog asking me to select the base template I want to navigate to:
I chose one of the base templates, and clicked ‘OK’:
I was then brought to the base template I had chosen:
If you have any thoughts on this, please leave a comment.
Each time you customize sitecore with no good reason a little kitten starts crying.
LOL!
It’s a really nice guide on how to customize sitecore and I can see using this as a template on how to do other customizations! (now I just have to get past knowing the Inheritance tab does the same thing already) 🙂
great job