The other day I read this post where the author showcased a new Clipboard command he had added into Sitecore Rocks, and immediately wanted to experiment with adding my own custom command into Sitecore Rocks.
After some research, I stumbled upon this post which gave a walk-through on augmenting Sitecore Rocks by adding a Server Component — this is an assembled library of code for your Sitecore instance to handle requests from Sitecore Rocks — and a Plugin — this is an assembled library of code that can contain custom commands — and decided to follow its lead.
I first created a Sitecore Rocks Server Component project in Visual Studio:
After some pondering, I decided to ‘cut my teeth’ on creating custom commands to protect and unprotect Items in Sitecore (for more information on protecting/unprotecting Items in Sitecore, check out ‘How to Protect or Unprotect an Item’ in Sitecore’s Client Configuration
Cookbook).
I decided to use the template method pattern for the classes that will handle requests from Sitecore Rocks — I envisioned some shared logic across the two — and put this shared logic into the following base class:
using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; namespace Sitecore.Rocks.Server.Requests { public abstract class EditItem { public string Execute(string id, string databaseName) { Assert.ArgumentNotNullOrEmpty(id, "id"); return ExecuteEditItem(GetItem(id, databaseName)); } private string ExecuteEditItem(Item item) { Assert.ArgumentNotNull(item, "item"); item.Editing.BeginEdit(); string response = UpdateItem(item); item.Editing.EndEdit(); return response; } protected abstract string UpdateItem(Item item); private static Item GetItem(string id, string databaseName) { Assert.ArgumentNotNullOrEmpty(id, "id"); Database database = GetDatabase(databaseName); Assert.IsNotNull(database, string.Format("database: {0} does not exist!", databaseName)); return database.GetItem(id); } private static Database GetDatabase(string databaseName) { Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName"); return Factory.GetDatabase(databaseName); } } }
The EditItem base class above gets the Item in the requested database, and puts the Item into edit mode. It then passes the Item to the UpdateItem method — subclasses must implement this method — and then turns off edit mode for the Item.
As a side note, all Server Component request handlers must have a method named Execute.
For protecting a Sitecore item, I built the following subclass of the EditItem class above:
using Sitecore.Data.Items; namespace Sitecore.Rocks.Server.Requests.Attributes { public class ProtectItem : EditItem { protected override string UpdateItem(Item item) { item.Appearance.ReadOnly = true; return string.Empty; } } }
The ProtectItem class above just protects the Item passed to it.
I then built the following subclass of EditItem to unprotect an item passed to its UpdateItem method:
using Sitecore.Data.Items; namespace Sitecore.Rocks.Server.Requests.Attributes { public class UnprotectItem : EditItem { protected override string UpdateItem(Item item) { item.Appearance.ReadOnly = false; return string.Empty; } } }
I built the above Server Component solution, and put its resulting assembly into the /bin folder of my Sitecore instance.
I then created a Plugin solution to handle the protect/unprotect commands in Sitecore Rocks:
I created the following command to protect a Sitecore Item:
using System; using System.Linq; using Sitecore.VisualStudio.Annotations; using Sitecore.VisualStudio.Commands; using Sitecore.VisualStudio.Data; using Sitecore.VisualStudio.Data.DataServices; using SmartAssembly.SmartExceptionsCore; namespace Sitecore.Rocks.Sandbox.Commands { [Command] public class ProtectItemCommand : CommandBase { public ProtectItemCommand() { Text = "Protect Item"; Group = "Edit"; SortingValue = 4010; } public override bool CanExecute([CanBeNull] object parameter) { IItemSelectionContext context = null; bool canExecute = false; try { context = parameter as IItemSelectionContext; canExecute = context != null && context.Items.Count() == 1 && !IsProtected(context.Items.FirstOrDefault()); } catch (Exception ex) { StackFrameHelper.CreateException3(ex, context, this, parameter); throw; } return canExecute; } private static bool IsProtected(IItem item) { ItemVersionUri itemVersionUri = new ItemVersionUri(item.ItemUri, LanguageManager.CurrentLanguage, Sitecore.VisualStudio.Data.Version.Latest); Item item2 = item.ItemUri.Site.DataService.GetItemFields(itemVersionUri); foreach (Field field in item2.Fields) { if (string.Equals("__Read Only", field.Name, StringComparison.CurrentCultureIgnoreCase) && field.Value == "1") { return true; } } return false; } public override void Execute([CanBeNull] object parameter) { IItemSelectionContext context = null; try { context = parameter as IItemSelectionContext; IItem item = context.Items.FirstOrDefault(); item.ItemUri.Site.DataService.ExecuteAsync ( "Attributes.ProtectItem", CreateEmptyCallback(), new object[] { item.ItemUri.ItemId.ToString(), item.ItemUri.DatabaseName.ToString() } ); } catch (Exception ex) { StackFrameHelper.CreateException3(ex, context, this, parameter); throw; } } private ExecuteCompleted CreateEmptyCallback() { return (response, executeResult) => { return; }; } } }
The ProtectItemCommand command above is only displayed when the selected Item is not protected — this is ascertained by logic in the CanExecute method — and fires off a request to the Sitecore.Rocks.Server.Requests.Attributes.ProtectItem request handler in the Server Component above to protect the selected Item.
I then built the following command to do the exact opposite of the command above: only appear when the selected Item is protected, and make a request to Sitecore.Rocks.Server.Requests.Attributes.UnprotectItem — shown above in the Server Component — to unprotect the selected Item:
using System; using System.Linq; using Sitecore.VisualStudio.Annotations; using Sitecore.VisualStudio.Commands; using Sitecore.VisualStudio.Data; using Sitecore.VisualStudio.Data.DataServices; using SmartAssembly.SmartExceptionsCore; namespace Sitecore.Rocks.Sandbox.Commands { [Command] public class UnprotectItemCommand : CommandBase { public UnprotectItemCommand() { Text = "Unprotect Item"; Group = "Edit"; SortingValue = 4020; } public override bool CanExecute([CanBeNull] object parameter) { IItemSelectionContext context = null; bool canExecute = false; try { context = parameter as IItemSelectionContext; canExecute = context != null && context.Items.Count() == 1 && IsProtected(context.Items.FirstOrDefault()); } catch (Exception ex) { StackFrameHelper.CreateException3(ex, context, this, parameter); throw; } return canExecute; } private static bool IsProtected(IItem item) { ItemVersionUri itemVersionUri = new ItemVersionUri(item.ItemUri, LanguageManager.CurrentLanguage, Sitecore.VisualStudio.Data.Version.Latest); Item item2 = item.ItemUri.Site.DataService.GetItemFields(itemVersionUri); foreach (Field field in item2.Fields) { if (string.Equals("__Read Only", field.Name, StringComparison.CurrentCultureIgnoreCase) && field.Value == "1") { return true; } } return false; } public override void Execute([CanBeNull] object parameter) { IItemSelectionContext context = null; try { context = parameter as IItemSelectionContext; IItem item = context.Items.FirstOrDefault(); item.ItemUri.Site.DataService.ExecuteAsync ( "Attributes.UnprotectItem", CreateEmptyCallback(), new object[] { item.ItemUri.ItemId.ToString(), item.ItemUri.DatabaseName.ToString() } ); } catch (Exception ex) { StackFrameHelper.CreateException3(ex, context, this, parameter); throw; } } private ExecuteCompleted CreateEmptyCallback() { return (response, executeResult) => { return; }; } } }
I had to do a lot of discovery in Sitecore.Rocks.dll via .NET Reflector in order to build the above commands, and had a lot of fun while searching and learning.
Unfortunately, I could not get the commands above to show up in the Sitecore Explorer context menu in my instance of Sitecore Rocks even though my plugin did make its way out to my C:\Users\[my username]\AppData\Local\Sitecore\Sitecore.Rocks\Plugins\ folder.
I troubleshooted for some time but could not determine why these commands were not appearing — if you have any ideas, please leave a comment — and decided to register my commands using Extensions in Sitecore Rocks as a fallback plan:
After clicking ‘Extensions’ in the Sitecore dropdown menu in Visual Studio, I was presented with the following dialog, and added my classes via the ‘Add’ button on the right:
Let’s see this in action.
I first created a Sitecore Item for testing:
I navigated to that Item in the Sitecore Explorer in Sitecore Rocks, and right-clicked on it:
After clicking ‘Protect Item’, I verified the Item was protected in Sitecore:
I then went back to our test Item in the Sitecore Explorer of Sitecore Rocks, and right-clicked again:
After clicking ‘Unprotect Item’, I took a look at the Item in Sitecore, and saw that it was no longer protected:
If you have any thoughts on this, or ideas for other commands that you would like to see in Sitecore Rocks, please drop a comment.
Until next time, have a Sitecoretastic day, and don’t forget: Sitecore Rocks!