Home » Commands (Page 2)
Category Archives: Commands
Restart the Sitecore Client and Server Using Custom Pipelines
Last week Michael West asked me about creating shortcuts to restart the Sitecore client and server via this tweet, and I was immediately curious myself on what was needed to accomplish this.
If you are unfamiliar with restarting the Sitecore client and server, these are options that are presented to Sitecore users — typically developers — after installing a package into Sitecore:
Until last week, I never understood how either of these checkboxes worked, and uncovered the following code during my research:
Michael West had conveyed how he wished these two lines of code lived in pipelines, and that prompted me to write this article — basically encapsulate the logic above into two custom pipelines: one to restart the Sitecore client, and the other to restart the Sitecore server.
I decided to define the concept of a ‘Restarter’, an object that restarts something — this could be anything — and defined the following interface for such objects:
namespace Sitecore.Sandbox.Utilities.Restarters { public interface IRestarter { void Restart(); } }
I then created the following IRestarter for the Sitecore client:
using Sitecore; using Sitecore.Diagnostics; using Sitecore.Web.UI.Sheer; namespace Sitecore.Sandbox.Utilities.Restarters { public class SitecoreClientRestarter : IRestarter { private ClientResponse ClientResponse { get; set; } public SitecoreClientRestarter() : this(Context.ClientPage) { } public SitecoreClientRestarter(ClientPage clientPage) { SetClientResponse(clientPage); } public SitecoreClientRestarter(ClientResponse clientResponse) { SetClientResponse(clientResponse); } private void SetClientResponse(ClientPage clientPage) { Assert.ArgumentNotNull(clientPage, "clientPage"); SetClientResponse(clientPage.ClientResponse); } private void SetClientResponse(ClientResponse clientResponse) { Assert.ArgumentNotNull(clientResponse, "clientResponse"); ClientResponse = clientResponse; } public void Restart() { ClientResponse.Broadcast(ClientResponse.SetLocation(string.Empty), "Shell"); } } }
The class above has three constructors. One constructor takes an instance of Sitecore.Web.UI.Sheer.ClientResponse — this lives in Sitecore.Kernel.dll — and another constructor takes in an instance of
Sitecore.Web.UI.Sheer.ClientPage — this also lives in Sitecore.Kernel.dll — which contains a property instance of Sitecore.Web.UI.Sheer.ClientResponse, and this instance is set on the ClientResponse property of the SitecoreClientRestarter class.
The third constructor — which is parameterless — calls the constructor that takes in a Sitecore.Web.UI.Sheer.ClientPage instance, and passes the ClientResponse instance set in Sitecore.Context.
I followed building the above class with another IRestarter — one that restarts the Sitecore server:
using Sitecore.Install; namespace Sitecore.Sandbox.Utilities.Restarters { public class SitecoreServerRestarter : IRestarter { public SitecoreServerRestarter() { } public void Restart() { Installer.RestartServer(); } } }
There really isn’t much happening in the class above. It just calls the static method RestartServer() — this method changes the timestamp on the Sitecore instance’s Web.config to trigger a web application restart — on Sitecore.Install.Installer in Sitecore.Kernel.dll.
Now we need to a way to use the IRestarter classes above. I built the following class to serve as a processor of custom pipelines I define later on in this post:
using Sitecore.Diagnostics; using Sitecore.Pipelines; using Sitecore.Sandbox.Utilities.Restarters; namespace Sitecore.Sandbox.Pipelines.RestartRestarter { public class RestartRestarterOperations { private IRestarter Restarter { get; set; } public void Process(PipelineArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(Restarter, "Restarter"); Restarter.Restart(); } } }
Through a pipeline processor configuration setting, we define the type of IRestarter — it’s magically created by Sitecore when the pipeline processor instance is created.
After some null checks, the Process() method invokes Restart() on the IRestarter instance, ultimately restarting whatever the IRestarter is set to restart.
I then needed a way to test the pipelines I define later on in this post. I built the following class to serve as commands that I added into the Sitecore ribbon:
using Sitecore.Diagnostics; using Sitecore.Pipelines; using Sitecore.Shell.Framework.Commands; namespace Sitecore.Sandbox.Commands.Admin { public class Restart : Command { public override void Execute(CommandContext context) { Assert.ArgumentNotNull(context, "context"); Assert.ArgumentNotNull(context.Parameters, "context.Parameters"); Assert.ArgumentNotNullOrEmpty(context.Parameters["pipeline"], "context.Parameters[\"pipeline\"]"); CorePipeline.Run(context.Parameters["pipeline"], new PipelineArgs()); } } }
The command above expects a pipeline name to be supplied via the Parameters NameValueCollection instance set on the CommandContext instance passed to the Execute method() — I show later on in this post how I pass the name of the pipeline to the command.
If the pipeline name is given, we invoke the pipeline.
I then glued everything together using the following configuration file:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <commands> <command name="admin:Restart" type="Sitecore.Sandbox.Commands.Admin.Restart, Sitecore.Sandbox"/> </commands> <pipelines> <restartClient> <processor type="Sitecore.Sandbox.Pipelines.RestartRestarter.RestartRestarterOperations, Sitecore.Sandbox"> <Restarter type="Sitecore.Sandbox.Utilities.Restarters.SitecoreClientRestarter, Sitecore.Sandbox"/> </processor> </restartClient> <restartServer> <processor type="Sitecore.Sandbox.Pipelines.RestartRestarter.RestartRestarterOperations, Sitecore.Sandbox"> <Restarter type="Sitecore.Sandbox.Utilities.Restarters.SitecoreServerRestarter, Sitecore.Sandbox"/> </processor> </restartServer> </pipelines> </sitecore> </configuration>
I’m omitting how I created custom buttons in a custom ribbon in Sitecore to test this. If you want to learn about adding buttons to the Sitecore Ribbon, please read John West’s blog post on doing so.
However, I did do the following to pass the name of the pipeline we want to invoke in the custom command class defined above:
I wish I could show you some screenshots on how this works. However, there really isn’t much visual to see here.
If you have any suggestions on how I could show this in action, or improve the code above, please share in a comment.
Choose Template Fields to Display in the Sitecore Content Editor
The other day I was going through search terms people had used to get to my blog, and discovered a few people made their way to my blog by searching for ‘sitecore hide sections in data template’.
I had built something like this in the past, but no longer remember how I had implemented that particular solution — not that I could show you that solution since it’s owned by a previous employer — and decided I would build another solution to accomplish this.
Before I move forward, I would like to point out that Sitecore MVP Andy Uzick wrote a blog post recently showing how to hide fields and sections in the content editor, though I did not have much luck with hiding sections in the way that he had done it — sections with no fields were still displaying for me in the content editor — and I decided to build a different solution altogether to make this work.
I thought it would be a good idea to let users turn this functionality on and off via a checkbox in the ribbon, and used some code from a previous post — in this post I had build an object to keep track of the state of a checkbox in the ribbon — as a model. In the spirit of that object, I defined the following interface:
namespace Sitecore.Sandbox.Utilities.ClientSettings { public interface IRegistrySettingToggle { bool IsOn(); void TurnOn(); void TurnOff(); } }
I then created the following abstract class which implements the interface above, and stores the state of the setting defined by the given key — the key of the setting and the “on” value are passed to it via a subclass — in the Sitecore registry.
using Sitecore.Diagnostics; using Sitecore.Web.UI.HtmlControls; namespace Sitecore.Sandbox.Utilities.ClientSettings { public abstract class RegistrySettingToggle : IRegistrySettingToggle { private string RegistrySettingKey { get; set; } private string RegistrySettingOnValue { get; set; } protected RegistrySettingToggle(string registrySettingKey, string registrySettingOnValue) { SetRegistrySettingKey(registrySettingKey); SetRegistrySettingOnValue(registrySettingOnValue); } private void SetRegistrySettingKey(string registrySettingKey) { Assert.ArgumentNotNullOrEmpty(registrySettingKey, "registrySettingKey"); RegistrySettingKey = registrySettingKey; } private void SetRegistrySettingOnValue(string registrySettingOnValue) { Assert.ArgumentNotNullOrEmpty(registrySettingOnValue, "registrySettingOnValue"); RegistrySettingOnValue = registrySettingOnValue; } public bool IsOn() { return Registry.GetString(RegistrySettingKey) == RegistrySettingOnValue; } public void TurnOn() { Registry.SetString(RegistrySettingKey, RegistrySettingOnValue); } public void TurnOff() { Registry.SetString(RegistrySettingKey, string.Empty); } } }
I then built the following class to toggle the display settings for our displayable fields:
using System; namespace Sitecore.Sandbox.Utilities.ClientSettings { public class ShowDisplayableFieldsOnly : RegistrySettingToggle { private const string RegistrySettingKey = "/Current_User/Content Editor/Show Displayable Fields Only"; private const string RegistrySettingOnValue = "on"; private static volatile IRegistrySettingToggle current; private static object lockObject = new Object(); public static IRegistrySettingToggle Current { get { if (current == null) { lock (lockObject) { if (current == null) { current = new ShowDisplayableFieldsOnly(); } } } return current; } } private ShowDisplayableFieldsOnly() : base(RegistrySettingKey, RegistrySettingOnValue) { } } }
It passes its Sitecore registry key and “on” state value to the RegistrySettingToggle base class, and employs the Singleton pattern — I saw no reason for there to be multiple instances of this object floating around.
In order to use a checkbox in the ribbon, we have to create a new command for it:
using System.Linq; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Shell.Framework.Commands; using Sitecore.Sandbox.Utilities.ClientSettings; namespace Sitecore.Sandbox.Commands { public class ToggleDisplayableFieldsVisibility : Command { public override void Execute(CommandContext context) { Assert.ArgumentNotNull(context, "context"); ToggleDisplayableFields(); Refresh(context); } private static void ToggleDisplayableFields() { IRegistrySettingToggle showDisplayableFieldsOnly = ShowDisplayableFieldsOnly.Current; if (!showDisplayableFieldsOnly.IsOn()) { showDisplayableFieldsOnly.TurnOn(); } else { showDisplayableFieldsOnly.TurnOff(); } } public override CommandState QueryState(CommandContext context) { if (!ShowDisplayableFieldsOnly.Current.IsOn()) { return CommandState.Enabled; } return CommandState.Down; } private static void Refresh(CommandContext context) { Refresh(GetItem(context)); } private static void Refresh(Item item) { Assert.ArgumentNotNull(item, "item"); Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", item.ID), 1); } private static Item GetItem(CommandContext context) { Assert.ArgumentNotNull(context, "context"); return context.Items.FirstOrDefault(); } } }
The command above leverages the instance of the ShowDisplayableFieldsOnly class defined above to turn the displayable fields feature on and off.
I followed the creation of the command above with the definition of the ribbon checkbox in the core database:
The command name above — which is set in the Click field — is defined in the patch configuration file towards the end of this post.
I then created the following data template with a TreelistEx field to store the displayable fields:
The TreelistEx field above will pull in all sections and their fields into the TreelistEx dialog, but only allow the selection of template fields, as is dictated by the following parameters that I have mapped in its Source field:
DataSource=/sitecore/templates/Sample/Sample Item&IncludeTemplatesForSelection=Template field&IncludeTemplatesForDisplay=Template section,Template field&AllowMultipleSelection=no
I then set this as a base template in my sandbox solution’s Sample Item template:
In order to remove fields, we need a <getContentEditorFields> pipeline processor. I built the following class for to serve as one:
using System; using Sitecore.Configuration; using Sitecore.Data.Fields; using Sitecore.Data.Items; using Sitecore.Data.Managers; using Sitecore.Data.Templates; using Sitecore.Diagnostics; using Sitecore.Shell.Applications.ContentManager; using Sitecore.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields; using Sitecore.Sandbox.Utilities.ClientSettings; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields { public class RemoveUndisplayableFields { public void Process(GetContentEditorFieldsArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Item, "args.Item"); Assert.ArgumentCondition(!string.IsNullOrWhiteSpace(DisplayableFieldsFieldName), "DisplayableFieldsFieldName", "DisplayableFieldsFieldName must be set in the configuration file!"); if (!ShowDisplayableFieldsOnly.Current.IsOn()) { return; } foreach (Editor.Section section in args.Sections) { AddDisplayableFields(args.Item[DisplayableFieldsFieldName], section); } } private void AddDisplayableFields(string displayableFieldIds, Editor.Section section) { Editor.Fields displayableFields = new Editor.Fields(); foreach (Editor.Field field in section.Fields) { if (IsDisplayableField(displayableFieldIds, field)) { displayableFields.Add(field); } } section.Fields.Clear(); section.Fields.AddRange(displayableFields); } private bool IsDisplayableField(string displayableFieldIds, Editor.Field field) { if (IsStandardValues(field.ItemField.Item)) { return true; } if (IsDisplayableFieldsField(field.ItemField)) { return false; } return IsStandardTemplateField(field.ItemField) || string.IsNullOrWhiteSpace(displayableFieldIds) || displayableFieldIds.Contains(field.ItemField.ID.ToString()); } private bool IsDisplayableFieldsField(Field field) { return string.Equals(field.Name, DisplayableFieldsFieldName, StringComparison.CurrentCultureIgnoreCase); } private static bool IsStandardValues(Item item) { if (item.Template.StandardValues != null) { return item.Template.StandardValues.ID == item.ID; } return false; } private bool IsStandardTemplateField(Field field) { Assert.IsNotNull(StandardTemplate, "The Stardard Template could not be found."); return StandardTemplate.ContainsField(field.ID); } private static Template GetStandardTemplate() { return TemplateManager.GetTemplate(Settings.DefaultBaseTemplate, Context.ContentDatabase); } private Template _StandardTemplate; private Template StandardTemplate { get { if (_StandardTemplate == null) { _StandardTemplate = GetStandardTemplate(); } return _StandardTemplate; } } private string DisplayableFieldsFieldName { get; set; } } }
The class above iterates over all fields for the supplied item, and adds only those that were selected in the Displayable Fields TreelistEx field, and also Standard Fields — we don’t want to remove these since they are shown/hidden by the Standard Fields feature in Sitecore — to a new Editor.Fields collection. This new collection is then set on the GetContentEditorFieldsArgs instance.
Plus, we don’t want to show the Displayable Fields TreelistEx field when the feature is turned on, and we are on an item. This field should only display when we are on the standard values item when the feature is turned on — this is how we will choose our displayable fields.
Now we have to handle sections without fields — especially after ripping them out via the pipeline processor above.
I built the following class to serve as a <renderContentEditor> pipeline processor to do this:
using System.Linq; using Sitecore.Diagnostics; using Sitecore.Shell.Applications.ContentManager; using Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor { public class FilterSectionsWithFields { public void Process(RenderContentEditorArgs args) { Assert.ArgumentNotNull(args, "args"); args.Sections = GetSectionsWithFields(args.Sections); } private static Editor.Sections GetSectionsWithFields(Editor.Sections sections) { Assert.ArgumentNotNull(sections, "sections"); Editor.Sections sectionsWithFields = new Editor.Sections(); foreach (Editor.Section section in sections) { AddIfContainsFields(sectionsWithFields, section); } return sectionsWithFields; } private static void AddIfContainsFields(Editor.Sections sections, Editor.Section section) { Assert.ArgumentNotNull(sections, "sections"); Assert.ArgumentNotNull(section, "section"); if (!ContainsFields(section)) { return; } sections.Add(section); } private static bool ContainsFields(Editor.Section section) { Assert.ArgumentNotNull(section, "section"); return section.Fields != null && section.Fields.Any(); } } }
It basically builds a new collection of sections that contain at least one field, and sets it on the RenderContentEditorArgs instance being passed through the <renderContentEditor> pipeline.
In order for this to work, we must make this pipeline processor run before all other processors.
I tied all of the above together with the following patch configuration file:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <commands> <command name="contenteditor:ToggleDisplayableFieldsVisibility" type="Sitecore.Sandbox.Commands.ToggleDisplayableFieldsVisibility, Sitecore.Sandbox"/> </commands> <pipelines> <getContentEditorFields> <processor type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields.RemoveUndisplayableFields, Sitecore.Sandbox"> <DisplayableFieldsFieldName>Displayable Fields</DisplayableFieldsFieldName> </processor> </getContentEditorFields> <renderContentEditor> <processor patch:before="processor[@type='Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderSkinedContentEditor, Sitecore.Client']" type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.FilterSectionsWithFields, Sitecore.Sandbox"/> </renderContentEditor> </pipelines> </sitecore> </configuration>
Let’s try this out.
I navigated to the standard values item for my Sample Item data template, and selected the fields I want to display in the content editor:
I then went to an item that uses this data template, and turned on the displayable fields feature:
As you can see, only the fields we had chosen display — along with standard fields since the Standard Fields checkbox is checked.
I then turned off the displayable fields feature:
Now all fields for the item display.
I then turned the displayable fields feature back on, and turned off Standard Fields:
Now only our selected fields display.
If you have any thoughts on this, or ideas around making this better, please share in a comment.
Expand Tokens on Sitecore Items Using a Custom Command in Sitecore PowerShell Extensions
During my Sitecore from the Command Line presentation at the Sitecore User Group – New England, I had shown attendees how they could go about adding a custom command into the Sitecore PowerShell Extensions module.
This blog post shows what I had presented — although the code in this post is an improved version over what I had presented at my talk. Many thanks to Sitecore MVP Adam Najmanowicz for helping me make this code better!
The following command will expand “out of the box” tokens in all fields of a supplied Sitecore item — check out Expand Tokens on Sitecore Items Using a Custom Command in Revolver where I discuss the problem commands like this address, and this article by Sitecore MVP Jens Mikkelsen which lists “out of the box” tokens available in Sitecore:
using System; using System.Management.Automation; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Cognifide.PowerShell.PowerShellIntegrations.Commandlets; namespace CommandLineExtensions.PowerShell.Commandlets { [Cmdlet("Expand", "Token")] [OutputType(new[] { typeof(Item) })] public class ExpandTokenCommand : BaseCommand { private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer(); [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)] public Item Item { get; set; } protected override void ProcessRecord() { Item.Editing.BeginEdit(); try { TokenReplacer.ReplaceItem(Item); Item.Editing.EndEdit(); } catch (Exception ex) { Item.Editing.CancelEdit(); throw ex; } WriteItem(Item); } } }
The command above subclasses Cognifide.PowerShell.PowerShellIntegrations.Commandlets.BaseCommand — the base class for most (if not all) commands in Sitecore PowerShell Extensions.
An item is passed to the command via a parameter, and is magically set on the Item property of the command class instance.
The ValueFromPipeline parameter being set to “true” on the Item property’s Parameter attribute will allow for chaining of this command with others so that items can be fed into it via a pipe bridging the commands together in PowerShell.
An instance of the Sitecore.Data.MasterVariablesReplacer class — which is created by the GetMasterVariablesReplacer() method of the Sitecore.Configuration.Factory class based on the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — is used to expand tokens on the supplied Sitecore item after the item was flagged for editing.
Once tokens have been expanded on the item — or not in the event an exception is encountered — the item is written to the Results window via the WriteItem method which is defined in the BaseCommand class.
I then had to wire up the custom command via a patch configuration file:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <powershell> <commandlets> <add Name="Custom Commandlets" type="*, CommandLineExtensions" /> </commandlets> </powershell> </sitecore> </configuration>
Let’s take this custom command for a spin.
I created a bunch of test items, and set tokens in their fields. I then selected the following page at random for testing:
I opened up the Integrated Scripting Environment of Sitecore PowerShell Extensions, typed in the following PowerShell code, and executed by pressing Ctrl-E:
As you can see tokens were expanded on the Page One item:
How about expanding tokens on all descendants of the Home item? Let’s see an example of how we can do that.
I chose the following content item — a grandchild of the Home item — for testing:
I switched back over to the Integrated Scripting Environment, wrote the following code for testing — the Get-ChildItem command with the -r parameter (this means do this recursively) will grab all descendants of the Home item, and pipe each item in the result set into the Expand-Token command — and clicked the Execute button:
I then went back to the grandchild item of the Home page in the content tree, and saw that tokens were expanded in its fields:
If you have any thoughts or comments on this, or ideas for new commands in Sitecore PowerShell Extensions, please share in a comment.
Until next time, have a scriptolicious day!
Expand Tokens on Sitecore Items Using a Custom Command in Revolver
On September 18, 2013, I presented Sitecore from the Command Line at the Sitecore User Group – New England.
During my presentation, I gave an example of creating a custom command in Revolver — the first scripting platform for Sitecore built by Alistair Deneys — and thought I would write something up for those who had missed the presentation, or wanted to revisit what I had shown.
One thing that plagues some Sitecore developers — if you disagree please leave a comment — is not having a nice way to expand tokens on items when tokens are added to Standard Values after items had been created previously.
Newly added tokens “bleed” into preexisting items’ fields, and I’ve seen developers perform crazy feats of acrobatic gymnastics to expand them — writing a standalone web form to recursive crawl the content tree to expand these is such an example (take a look at Empower Your Content Authors to Expand Standard Values Tokens in the Sitecore Client where I offer an alternative way to expand tokens on content items).
The following custom Revolver command will expand tokens on a supplied Sitecore item, and help out on the front of expanding newly added tokens on preexisting items:
using System; using Sitecore.Configuration; using System.Linq; using Sitecore.Data; using Sitecore.Data.Items; using Revolver.Core; using Revolver.Core.Commands; namespace CommandLineExtensions.Revolver.Commands { public class ExpandTokensCommand : BaseCommand { private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer(); public override string Description() { return "Expand tokens on an item"; } public override HelpDetails Help() { HelpDetails details = new HelpDetails { Description = Description(), Usage = "<cmd> [path]" }; details.AddExample("<cmd>"); details.AddExample("<cmd> /item1/item2"); return details; } public override CommandResult Run(string[] args) { string path = string.Empty; if (args.Any()) { path = args.FirstOrDefault(); } using (new ContextSwitcher(Context, path)) { if (!Context.LastGoodPath.EndsWith(path, StringComparison.CurrentCultureIgnoreCase)) { return new CommandResult ( CommandStatus.Failure, string.Format("Failed to expand tokens on item {0}\nReason:\n\n An item does not exist at that location!", path) ); } CommandResult result; Item item = Context.CurrentItem; item.Editing.BeginEdit(); try { TokenReplacer.ReplaceItem(item); result = new CommandResult(CommandStatus.Success, string.Concat("Expanded tokens on item ", Context.LastGoodPath)); item.Editing.EndEdit(); } catch (Exception ex) { item.Editing.CancelEdit(); result = new CommandResult(CommandStatus.Failure, string.Format("Failed to expand tokens on item {0}\nReason:\n\n{1}", path, ex)); } return result; } } } }
Tokens are expanded using an instance of the Sitecore.Data.MasterVariablesReplacer class — you can roll your own, and wire it up in the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — which is provided by Sitecore.Configuration.Factory.GetMasterVariablesReplacer().
All custom commands in Revolver must implement the Revolver.Core.ICommand interface. I subclassed Revolver.Core.Commands.BaseCommand — which does implement this interface — since it seemed like the right thing to do given that all “out of the box” commands I saw in Revolver were subclassing it, and then implemented the Description(), Help() and Run() abstract methods.
I then had to bind the custom command to a new name — I chose “et” for “Expand Tokens”:
@echooff @stoponerror bind CommandLineExtensions.Revolver.Commands.ExpandTokensCommand,CommandLineExtensions et @echoon
Since it wouldn’t be efficient to type and run this bind script every time I want to use the “et” command, I added it into a startup script in the core database:
I then had to create a user script for the startup script to run. I chose the Everyone role here for demonstration purposes:
The above startup script will be invoked when Revolver is opened, and our custom command will be bound.
Let’s see all of the above in action.
I added some tokens in my home item:
I then opened up Revolver, navigated to /sitecore/content, and ran the custom command on the home item:
As you can see the tokens were expanded:
You might be thinking “that’s wonderful Mike — except now I have to navigate to every item in my content tree using Revolver, and then run this custom command on it”.
Well, I do have a solution for this: a custom script that grabs an item and all of its descendants using a Sitecore query, and passes them to the custom command to expand tokens:
@echooff @stoponerror if ($1$ = \$1\$) (exit (Missing required parameter path)) @echoon query -ns $1$/descendant-or-self::* et
I put this script in the core database, and named it “etr” for “Expand Tokens Recursively”:
I navigated to a descendant of /sitecore/content/home, and see that it has some unexpanded tokens on it:
I went back to Revolver, and ran the “etr” command on the home item:
As you can see tokens were expanded on the descendant item:
If you have any thoughts on this, or have ideas for other custom commands in Revolver, please share in a comment.
Navigate to Base Templates of a Template using a Sitecore Command
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.