Home » Search results for 'Command'

Search Results for: Command

Advertisements

Bucket Items in Sitecore using a Custom Commandlet in Sitecore PowerShell Extensions

Last Wednesday I had the privilege to present Sitecore PowerShell Extensions (SPE) at the Milwaukee Sitecore Meetup. During my presentation, I demonstrated how easy it is to add, execute and reuse PowerShell scripts in SPE, and I showcased version 3.0 of SPE on Sitecore XP 8.

Unfortunately, I ran out of time before showing how one can go about creating a custom commandlet in SPE, and hope to make it up to everyone by sharing the commandlet I wrote for the presentation in this post.

I wrote the following commandlet to convert an Item into an Item Bucket in Sitecore:

using System;
using System.Management.Automation;

using Sitecore.Data.Items;
using Sitecore.Shell.Framework.Commands;

using Cognifide.PowerShell.Commandlets;
using Cognifide.PowerShell.Commandlets.Interactive.Messages;

namespace Sitecore.Sandbox.SPE.Commandlets.Buckets
{
    [Cmdlet(VerbsData.ConvertTo, "Bucket"), OutputType(new Type[] { typeof(Item) })]
    public class ConvertToBucketCommand : BaseItemCommand
    {
        protected override void ProcessItem(Item item)
        {
            try
            {
                PutMessage(new ShellCommandInItemContextMessage(item, "item:bucket"));   
            }
            catch (Exception exception)
            {
                WriteError(new ErrorRecord(exception, "sitecore_new_bucket_error", ErrorCategory.NotSpecified, Item));
            }

            WriteItem(Item);
        }
    }
}

The above commandlet implements the ProcessItem() method — this method is declared abstract in one of the ancestor classes of the class above — and leverages the framework of SPE to invoke a Sheer UI command to bucket the Item passed to the method — one of the ancestor classes of this class passes the Item to be processed.

The above highlights how in SPE we are employing the Template method pattern for many “out of the box” commandlets. This involves inheriting from an abstract base class — Cognifide.PowerShell.Commandlets.BaseItemCommand in Cognifide.PowerShell.dll (this assembly comes with the SPE module) is an example of one of these base classes — and implementing methods that are defined as abstract. The parent or an ancestor class will do the brunt of the work behind the scenes, and use your method implementation for specifics.

As a side note, we also provide method hooks as well — these are virtual methods defined on a base or ancestor class — which you can override to change how they work to meet your particular needs.

I then wired the above up using a Sitecore include configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
        <add Name="Custom Bucket Commandlets" type="*, Sitecore.Sandbox.SPE" />
      </commandlets>
    </powershell>
  </sitecore>
</configuration>

I deployed the above to my Sitecore instance; loaded up the Integrated Scripting Environment (ISE) in SPE; and saw that my commandlet was registered using the Control-Space shortcut key:

convert-to-bucket-ise-control-space

Let’s take this for a spin. Let’s convert the Home Item into an Item Bucket:

home-before-bucket

Here’s my script to do that:

ise-convert-home-bucket

I clicked the execute button, and then got this confirmation dialog:

ise-convert-home-bucket-confirm

I then clicked the “Ok” button and was immediately presented with this dialog:

ise-convert-home-bucket-processing

As you can see it worked! The Home Item in my content tree is now an Item Bucket:

home-after-bucket

If you have any thoughts on this or ideas for other custom commandlets for SPE, please share in a comment.

If you would like to watch the Milwaukee Sitecore Meetup presentation where I showcased Sitecore PowerShell Extensions — and as a bonus you’ll also get to see some real-life application of SPE from Adam Brauer, Senior Product Engineer at Active Commerce, in this presentation as well — it has been recorded for posterity, and you can watch it here:

Until next time, stay curious, keep experimenting, and let’s keep on sharing all the Sitecore things!

Advertisements

Accept All Notifications on Clones of an Item using a Custom Command in Sitecore

As I was walking along a beach near my apartment tonight, I thought “wouldn’t it be nifty to have a button in the Sitecore ribbon to accept all notifications on clones of an Item instead of having to accept these manually on each clone?”

I immediately returned home, and whipped up the following command class:

using System.Collections.Generic;
using System.Linq;

using Sitecore.Data.Clones;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Shell.Framework.Commands
{
    public class AcceptAllNotificationsOnClones : Command
    {
        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            IEnumerable<Item> clones = GetClonesWithNotifications(GetItem(context));
            if(!clones.Any())
            {
                return CommandState.Hidden;
            }

            return CommandState.Enabled;
        }

        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Item item = GetItem(context);
            IEnumerable<Item> clones = GetClonesWithNotifications(item);
            if(!clones.Any())
            {
                return;
            }

            foreach (Item clone in clones)
            {
                AcceptAllNotifications(item.Database.NotificationProvider, clone);
            }
        }

        protected virtual Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            return context.Items.FirstOrDefault();
        }

        protected virtual IEnumerable<Item> GetClonesWithNotifications(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            IEnumerable<Item> clones = item.GetClones();
            if(!clones.Any())
            {
                return new List<Item>();
            }
            
            IEnumerable<Item> clonesWithNotifications = GetClonesWithNotifications(item.Database.NotificationProvider, clones);
            if(!clonesWithNotifications.Any())
            {
                return new List<Item>();
            }

            return clonesWithNotifications;
        }

        protected virtual IEnumerable<Item> GetClonesWithNotifications(NotificationProvider notificationProvider, IEnumerable<Item> clones)
        {
            Assert.ArgumentNotNull(notificationProvider, "notificationProvider");
            Assert.ArgumentNotNull(clones, "clones");
            return (from clone in clones
                    let notifications = notificationProvider.GetNotifications(clone)
                    where notifications.Any()
                    select clone).ToList();
        }

        protected virtual void AcceptAllNotifications(NotificationProvider notificationProvider, Item clone)
        {
            Assert.ArgumentNotNull(notificationProvider, "notificationProvider");
            Assert.ArgumentNotNull(clone, "clone");
            foreach (Notification notification in notificationProvider.GetNotifications(clone))
            {
                notification.Accept(clone);
            }
        }
    }
}

The code in the command above ensures the command is only visible when the selected Item in the Sitecore content tree has clones, and those clones have notifications — this visibility logic is contained in the QueryState() method.

When the command is invoked — this happens through the Execute() method — all clones with notifications of the selected Item are retrieved, and iterated over — each are passed to the AcceptAllNotifications() method which contains logic to accept all notifications on them via the Accept() method on a NotificationProvider instance: this NotificationProvider instance comes from the source Item’s Database property.

I then registered the above command class in Sitecore using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:AcceptAllNotificationsOnClones" type="Sitecore.Sandbox.Shell.Framework.Commands.AcceptAllNotificationsOnClones, Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

We need a way to invoke this command. I created a new button to go into the ‘Item Clones’ chunk in the ribbon:

accept-notifications-on-clones-button-core

Let’s take this for a test drive!

I first created some clones:

created-clones

I then changed a field value on one of those clones:

changed-field-value-on-clone

On the clone’s source Item, I changed the same field’s value with something completely different, and added a new child item — the new button appeared after saving the Item:

new-child-item-field-value-change

Now, the clone has notifications on it:

notifications-on-clone

I went back to the source Item, clicked the ‘Accept Notifications On Clones’ button in the ribbon, and navigated back to the clone:

notifications-accepted

As you can see, the notifications were accepted.

If you have any thoughts on this, please share in a comment.

Build a Custom Command in Sitecore PowerShell Extensions

In my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014, I showed the audience how easy it is to build custom commands for Sitecore PowerShell Extensions, and thought it would be a good idea to distill what I had shown into a blog post for future reference. This blog post embodies that endeavor.

During my presentation, I shared an example of using the template method pattern for two commands using the following base class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;

using Sitecore.Data.Items;

using Cognifide.PowerShell.PowerShellIntegrations.Commandlets;

namespace Sitecore.Sandbox.SPE.Commandlets.Data
{
    public abstract class EditItemCommand : BaseCommand
    {
        protected override void ProcessRecord()
        {
            ProcessItem(Item);
            if (!Recurse.IsPresent)
            {
                return;
            }
            
            ProcessItems(Item.Children, true);
        }

        private void ProcessItems(IEnumerable<Item> items, bool recursive)
        {
            foreach (Item item in items)
            {
                ProcessItem(item);
                if (recursive && item.Children.Any())
                {
                    ProcessItems(item.Children, recursive);
                }
            }
        }

        private void ProcessItem(Item item)
        {
            item.Editing.BeginEdit();
            try
            {
                EditItem(item);
                item.Editing.EndEdit();
            }
            catch (Exception exception)
            {
                item.Editing.CancelEdit();
                throw exception;
            }

            WriteItem(item);
        }

        protected abstract void EditItem(Item item);

        [Parameter(ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
        public Item Item { get; set; }

        [Parameter]
        public SwitchParameter Recurse { get; set; }
    }
}

The class above defines the basic algorithm for editing an Item — the editing part occurs in the EditItem() method which must be defined by subclasses — and all of its descendants when the Recurse switch is supplied to the command. When the Recursive switch is supplied, recursion is employed to process all descendants of the Item once editing of the supplied Item is complete.

The following subclass of the EditItemCommand class above protects a supplied Item in its implementation of the EditItem() method:

using System;
using System.Management.Automation;

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.SPE.Commandlets.Data
{
    [OutputType(new Type[] { typeof(Sitecore.Data.Items.Item) }), Cmdlet("Protect", "Item")]
    public class ProtectItemCommand : EditItemCommand
    {
        protected override void EditItem(Item item)
        {
            item.Appearance.ReadOnly = true;
        }
    }
}

Conversely, the following subclass of the EditItemCommand class unprotects the passed Item in its EditItem() method implementation:

using System;
using System.Management.Automation;

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.SPE.Commandlets.Data
{
    [OutputType(new Type[] { typeof(Sitecore.Data.Items.Item) }), Cmdlet("Unprotect", "Item")]
    public class UnprotectItemCommand : EditItemCommand
    {
        protected override void EditItem(Item item)
        {
            item.Appearance.ReadOnly = false;
        }
    }
}

The verb and noun for each command is defined in the Cmdlet class attribute set on each command class declaration.

I then registered all of the above in Sitecore using the following configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
            <add Name="Sitecore Sandbox Commandlets" type="*, Sitecore.Sandbox" />
      </commandlets>
    </powershell>
  </sitecore>                                                                                    
</configuration>

Since everything looks copacetic — you got to love a developer’s optimism 😉 — I built and deployed all of the above to my Sitecore instance.

Let’s take this for a spin!

I selected my home Item knowing it is not protected:

not-protected-home

I then looked to see if it had an unprotected descendant, and found the following item:

not-protected-page-three

I then ran a script on the home Item using our new command to protect an item, and supplied the Recurse switch to protect all descendants:

protect-item-command-powershell-ise

As you can see, the home Item is now protected:

protected-home

Its descendant is also protected:

protected-page-three

Let’s now unprotect them. I ran a script on the home Item using our new command to unprotect an item, and supplied the Recurse switch to process all descendants:

unprotect-item-command-powershell-ise

As you can see, the home Item is now unprotected again:

not-protected-again-home

Its descendant is also unprotected:

not-protected-again-page-three

If you have any thoughts or ideas around improving anything you’ve seen in this post, or have other ideas for commands that should be included in Sitecore PowerShell Extensions, please drop a comment.

I would also like to point out that I had written a previous blog post on creating a custom command in Sitecore PowerShell Extensions. You might want to go check that out as well.

Until next time, have a scriptastic day! 🙂

Add Sitecore Rocks Commands to Protect and Unprotect Items

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:

sitecore-rocks-server-component

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:

sitecore-rocks-plugin

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:

sitecore-rocks-extensions-menu-option

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:

sitecore-rocks-extension-dialog

Let’s see this in action.

I first created a Sitecore Item for testing:

item-unprotected

I navigated to that Item in the Sitecore Explorer in Sitecore Rocks, and right-clicked on it:

item-unprotected-1

After clicking ‘Protect Item’, I verified the Item was protected in Sitecore:

item-protected

I then went back to our test Item in the Sitecore Explorer of Sitecore Rocks, and right-clicked again:

sitecore-rocks-unprotect-item

After clicking ‘Unprotect Item’, I took a look at the Item in Sitecore, and saw that it was no longer protected:

item-unprotected-again

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!

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:

page-one-unexpanded-tokens

I opened up the Integrated Scripting Environment of Sitecore PowerShell Extensions, typed in the following PowerShell code, and executed by pressing Ctrl-E:

spe-ise-expand-tokens-page-one

As you can see tokens were expanded on the Page One item:

page-one-expanded-tokens

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:

inner-page-one-unexpanded-tokens

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:

spe-ise-expand-on-descendants

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:

spe-expanded-on-descendants

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:

bind-custom-commands-script

I then had to create a user script for the startup script to run. I chose the Everyone role here for demonstration purposes:

bind-custom-commands-user-script

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:

home-item-unexpanded-tokens

I then opened up Revolver, navigated to /sitecore/content, and ran the custom command on the home item:

ran-et-revolver

As you can see the tokens were expanded:

home-item-expanded-tokens

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”:

etr-script

I navigated to a descendant of /sitecore/content/home, and see that it has some unexpanded tokens on it:

descendant-unexpanded-tokens

I went back to Revolver, and ran the “etr” command on the home item:

etr-revolver

As you can see tokens were expanded on the descendant item:

descendant-expanded-tokens

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?”

the-problem-1

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:

inheritance-tab

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:

meta-template

I also created a ‘Base Page’ template which uses the ‘Meta’ template above:

base-page-template

Next I created ‘The Coolest Page Template Ever’ template — this uses the ‘Base Page’ template as its base template:

the-coolest-page-template-ever-template

I then right-clicked on ‘The Coolest Page Template Ever’ template to launch its context menu, and selected our new menu option:

context-menu-go-to-base-template

I was then presented with a dialog asking me to select the base template I want to navigate to:

base-template-lister-modal-1

I chose one of the base templates, and clicked ‘OK’:

base-template-lister-modal-2

I was then brought to the base template I had chosen:

brought-to-selected-base-template

If you have any thoughts on this, please leave a comment.

Chain Together Sitecore Client Commands using a Composite Command

Today, I procrastinated on doing chores around the house by exploring whether one could chain together Sitecore client commands in order to reduce the number of clicks and/or keypresses required when invoking these commands separately — combining the click of the ‘Save’ button followed by one of the publishing buttons would be an example of this.

Immediately, the composite design pattern came to mind for a candidate solution — you can read more about this pattern in the the Gang of Four’s book on design patterns.

This high-level plan of attack lead to the following custom composite command.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Text;

namespace Sitecore.Sandbox.Commands
{
    public class CompositeCommand : Command
    {
        public override void Execute(CommandContext commandContext)
        {
            IEnumerable<Command> commands = GetCommands();

            foreach (Command command in commands)
            {
                command.Execute(commandContext);
            }
        }

        protected IEnumerable<Command> GetCommands()
        {
            ListString commandNames = GetCommandNames();
            IList<Command> commands = new List<Command>();

            foreach(string commandName in commandNames)
            {
                AddToListIfNotNull(commands, CommandManager.GetCommand(commandName));
            }

            return commands;
        }

        private ListString GetCommandNames()
        {
            return new ListString(GetCommandsFieldValue(), '|');
        }

        private string GetCommandsFieldValue()
        {
            XmlNode xmlNode = Factory.GetConfigNode(string.Format("commands/command[@name='{0}']", Name));
            bool canGetCommands = xmlNode != null && xmlNode.Attributes["commands"] != null;

            if (canGetCommands)
            {
                return xmlNode.Attributes["commands"].Value;
            }
            
            return string.Empty;
        }

        private static void AddToListIfNotNull<T>(IList<T> list, T objectToAdd) where T : class
        {
            Assert.ArgumentNotNull(list, "list");
            if (objectToAdd != null)
            {
                list.Add(objectToAdd);
            }
        }
    }
}

The above command — using the Sitecore.Configuration.Factory class — gets its XML configuration element; parses the list of commands it wraps from a new attribute I’ve added — I’ve named this attribute “commands”; gets instances of these commands via Sitecore.Shell.Framework.Commands.CommandManager (from Sitecore.Kernel.dll); and invokes all Command instances’ Execute() methods consecutively — ordered from left to right, separated by pipes, in the “commands” attribute on the command XML element in /App_Config/Commands.config.

To test this out, I thought I’d create a composite command combining the item save command with the publish command — the command that launches the Publish Item Wizard.

I first had to wire up my new command by adding a new command XML element to /App_Config/Commands.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<!-- bunch of commands up here --> 
	<command name="composite:savethenpublish" commands="contenteditor:save|item:publish" type="Sitecore.Sandbox.Commands.CompositeCommand,Sitecore.Sandbox"/>
</configuration>

Next, in the core db, I had to create a reference under the home strip:

save-publish-reference

Followed by a chunk containing a button that holds the name of our new command:

save-publish-chunk-button

Let’s see this new composite command in action. I did the following:

save-and-publish-item

After the Sitecore save animation completed, the Publish Item Wizard popped up:

save-publish-item-2

Now, it’s time for some fun. Let’s combine commands that make little sense in chaining together.

Let’s chain together:

  1. the command I built in my post on expanding Standard Values tokens
  2. the command to move an item before all its siblings in the content tree
  3. the command to move an item down in the content tree

Here’s what the command’s configuration looks like in /App_Config/Commands.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<!-- bunch of commands up here --> 
	<command name="composite:chainsomeunrelatedcommands" commands="item:expandtokens|item:movefirst|item:movedown" type="Sitecore.Sandbox.Commands.CompositeCommand,Sitecore.Sandbox"/>
</configuration>

I decided to add this to the item context menu — for more information on adding to the context menu for items, please check out my post that shows you how to do this — so I created new menu option in the context menu for items in the core database:

chain-command-context-menu-option

I switched back to the master database; added some Standard Values tokens to some fields in the Page 4 item I created for testing; right-clicked on on it; and clicked my new context menu option:

chain-context-menu-option-invoke

After a second or two, I saw that the tokens were expanded, and the item was moved:

chain-context-menu-option-finished

That’s all for now. It’s now time to go do some house chores. Otherwise, people might start thinking I’m truly addicted to building things in Sitecore. 🙂

You Can’t Move This! Experiments in Disabling Move Related Commands in the getQueryState Sitecore Pipeline

I was scavenging through my local sandbox instance’s Web.config the other day — yes I was looking for things to customize — and noticed the getQueryState pipeline — a pipeline that contains no “out of the box” pipeline processors:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
	<!-- Some stuff here -->
	<sitecore>
		<!-- Some more stuff here -->
		<pipelines>
			<!-- Even more stuff here -->
		<!--  Allows developers to programmatically disable or hide any button or panel in the Content Editor ribbons 
            without overriding the individual commands. 
            Processors must accept a single argument of type GetQueryStateArgs (namespace: Sitecore.Pipelines.GetQueryState)  -->
			<getQueryState>
			</getQueryState>
			<!-- Yeup, more stuff here -->
		</pipelines>
		<!-- wow, lots of stuff here too -->
	</sitecore>
	<!-- lots of stuff down here -->
</configuration>

The above abridged version of my Web.config contains an XML comment underscoring what this pipeline should be used for: disabling and/or hiding buttons in the Sitecore client.

Although I am still unclear around the practicality of using this pipeline overall — if you have an idea, please leave a comment — I thought it would be fun building one regardless, just to see how it works. Besides, I like to tinker with things — many of my previous posts corroborate this sentiment.

What I came up with is a getQueryState pipeline processor that disables buttons containing move related commands on a selected item in the content tree with a particular template. Sorting commands also fall under the umbrella of move related commands, so these are included in our set of commands to disable.

Here’s the pipeline processor I built:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetQueryState;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Pipelines.GetQueryState
{
    public class DisableMoveCommands
    {
        private static readonly IEnumerable<string> Commands = GetCommands();
        private static readonly IEnumerable<string> UnmovableTemplateIDs = GetUnmovableTemplateIDs();

        public void Process(GetQueryStateArgs args)
        {
            if (!CanProcessGetQueryStateArgs(args))
            {
                return;
            }

            bool shouldDisableCommand = IsUnmovableItem(GetCommandContextItem(args)) && IsMovableCommand(args.CommandName);
            if (shouldDisableCommand)
            {
                args.CommandState = CommandState.Disabled;
            }
        }

        private static bool CanProcessGetQueryStateArgs(GetQueryStateArgs args)
        {
            return args != null
                    && !string.IsNullOrEmpty(args.CommandName)
                    && args.CommandContext != null
                    && args.CommandContext.Items.Any();
        }

        private static Item GetCommandContextItem(GetQueryStateArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.CommandContext, "args.CommandContext");
            Assert.ArgumentNotNull(args.CommandContext.Items, "args.CommandContext.Items");
            return args.CommandContext.Items.FirstOrDefault();
        }

        private static bool IsUnmovableItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return IsUnmovableTemplateID(item.TemplateID);
        }

        private static bool IsUnmovableTemplateID(ID templateID)
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(templateID), "templateID", "templateID must be set!");
            return UnmovableTemplateIDs.Contains(templateID.ToString());
        }

        private static bool IsMovableCommand(string command)
        {
            Assert.ArgumentNotNullOrEmpty(command, "command");
            return Commands.Contains(command);
        }

        private static IEnumerable<string> GetCommands()
        {
            return GetStringCollection("moveCommandsToPrevent/command");
        }

        private static IEnumerable<string> GetUnmovableTemplateIDs()
        {
            return GetStringCollection("unmovableTemplates/id");
        }

        private static IEnumerable<string> GetStringCollection(string path)
        {
            Assert.ArgumentNotNullOrEmpty(path, "path");
            return Factory.GetStringSet(path);
        }
    }
}

The above pipeline processor checks to see if the selected item in the content tree has the unmovable template I defined, coupled with whether the current command is within the set of commands we are to disable. If both are cases are met, the pipeline processor will disable the context command.

I defined my unmovable template and commands to disable in a patch include config file, along with the getQueryState pipeline processor:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getQueryState>
        <processor type="Sitecore.Sandbox.Pipelines.GetQueryState.DisableMoveCommands, Sitecore.Sandbox" />
      </getQueryState>
    </pipelines>
    <unmovableTemplates>
      <id>{8ADD3F45-027C-49C5-A8FB-0406B8C8728D}</id>
    </unmovableTemplates>
    <moveCommandsToPrevent>
      <command>item:moveto</command>
      <command>item:cuttoclipboard</command>
      <command>item:moveup</command>
      <command>item:movedown</command>
      <command>item:movefirst</command>
      <command>item:movelast</command>
      <command>item:moveto</command>
    </moveCommandsToPrevent>
  </sitecore>
</configuration>

Looking at the context menu on my unmovable item — appropriately named “You Cant Move This” — you can see the move related buttons are disabled:

context-menu-move-commands-disabled

Plus, item level sorting commands are also disabled:

context-menu-sorting-commands-disabled

Move related buttons in the ribbon are also disabled:

move-commands-in-ribbon-disabled

There really wasn’t much to building this pipeline processor, and this pipeline is at your disposal if you ever find yourself in a situation where you might have to disable buttons in the Sitecore client for whatever reason.

However, as I mentioned above, I still don’t understand why one would want to use this pipeline. If you have an idea why, please let me know.

Write Sitecore Experience Forms Log Entries to a Custom SQL Server Database Table

Not long after I wrote the code for my last post, I continued exploring ways of changing service classes in Sitecore Experience Forms.

One thing that popped out when continuing on this quest was Sitecore.ExperienceForms.Diagnostics.ILogger. I immediately thought “I just wrote code for retrieving Forms configuration settings from a SQL Server database table, why not create a new ILogger service class for storing log entries in a custom SQL table?”

Well, that’s what I did, and the code in this post captures how I went about doing that.

You might be asking “Mike, you know you can just use a SQL appender in log4net, right?”

Well, I certainly could have but what fun would that have been?

Anyways, let’s get started.

We first need a class that represents a log entry. I created the following POCO class to serve that purpose:

using System;

namespace Sandbox.Foundation.Forms.Models.Logging
{
	public class LogEntry
	{
		public Exception Exception { get; set; }

		public string LogEntryType { get; set; }

		public string LogMessage { get; set; }

		public string Message { get; set; }

		public object Owner { get; set; }

		public DateTime CreatedDate { get; set; }
	}
}

Since I hate calling the “new” keyword when creating new instances of classes, I chose to create a factory class. The following interface will be for instances of classes that create LogEntry instances:

using System;

using Sandbox.Foundation.Forms.Models.Logging;

namespace Sandbox.Foundation.Forms.Services.Factories.Diagnostics
{
	public interface ILogEntryFactory
	{
		LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, Type ownerType, DateTime createdDate);
		
		LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, object owner, DateTime createdDate);

		LogEntry CreateLogEntry(string logEntryType, string message, Type ownerType, DateTime createdDate);

		LogEntry CreateLogEntry(string logEntryType, string message, object owner, DateTime createdDate);
	}
}

Well, we can’t do much with just an interface. The following class implements the interface above. It creates an instance of LogEntry with the passed parameters to all methods (assuming the required parameters are passed with the proper values on them):

using System;

using Sandbox.Foundation.Forms.Models.Logging;

namespace Sandbox.Foundation.Forms.Services.Factories.Diagnostics
{
	public class LogEntryFactory : ILogEntryFactory
	{
		public LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, Type ownerType, DateTime createdDate)
		{
			return CreateLogEntry(logEntryType, message, exception, ownerType, createdDate);
		}

		public LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, object owner, DateTime createdDate)
		{
			if (!CanCreateLogEntry(logEntryType, message, owner, createdDate))
			{
				return null;
			}

			return new LogEntry
			{
				LogEntryType = logEntryType,
				Message = message,
				Exception = exception,
				Owner = owner,
				CreatedDate = createdDate
			};
		}

		public LogEntry CreateLogEntry(string logEntryType, string message, Type ownerType, DateTime createdDate)
		{
			return CreateLogEntry(logEntryType, message, ownerType, createdDate);
		}

		public LogEntry CreateLogEntry(string logEntryType, string message, object owner, DateTime createdDate)
		{
			if(!CanCreateLogEntry(logEntryType, message, owner, createdDate))
			{
				return null;
			}

			return new LogEntry
			{
				LogEntryType = logEntryType,
				Message = message,
				Owner = owner,
				CreatedDate = createdDate
			};
		}

		protected virtual bool CanCreateLogEntry(string logEntryType, string message, object owner, DateTime createdDate)
		{
			return !string.IsNullOrWhiteSpace(logEntryType)
				&& !string.IsNullOrWhiteSpace(message)
				&& owner != null
				&& createdDate != DateTime.MinValue
				&& createdDate != DateTime.MaxValue;
		}
	}
}

I didn’t want to send LogEntry instances directly to a repository class instance directly, so I created the following class to represent the entities which will ultimately be stored in the database:

using System;

namespace Sandbox.Foundation.Forms.Models.Logging
{
	public class RepositoryLogEntry
	{
		public string LogEntryType { get; set; }

		public string LogMessage { get; set; }

		public DateTime Created { get; set; }
	}
}

As I had done with LogEntry, I created a factory class for it. The difference here is we will be passing an instance of LogEntry to this new factory so we can create a RepositoryLogEntry instance from it.

The following interface is for factories of RepositoryLogEntry:

using System;

using Sandbox.Foundation.Forms.Models.Logging;

namespace Sandbox.Foundation.Forms.Services.Factories.Diagnostics
{
	public interface IRepositoryLogEntryFactory
	{
		RepositoryLogEntry CreateRepositoryLogEntry(LogEntry entry);
		
		RepositoryLogEntry CreateRepositoryLogEntry(string logEntryType, string logMessage, DateTime created);
	}
}

Now that we have the interface ready to go, we need an implementation class for it. The following class does the job:

using System;

using Sandbox.Foundation.Forms.Models.Logging;

namespace Sandbox.Foundation.Forms.Services.Factories.Diagnostics
{
	public class RepositoryLogEntryFactory : IRepositoryLogEntryFactory
	{
		public RepositoryLogEntry CreateRepositoryLogEntry(LogEntry entry)
		{
			return CreateRepositoryLogEntry(entry.LogEntryType, entry.LogMessage, entry.CreatedDate);
		}

		public RepositoryLogEntry CreateRepositoryLogEntry(string logEntryType, string logMessage, DateTime created)
		{
			if (!CanCreateRepositoryLogEntry(logEntryType, logMessage, created))
			{
				return null;
			}

			return new RepositoryLogEntry
			{
				LogEntryType = logEntryType,
				LogMessage = logMessage,
				Created = created
			};
		}

		protected virtual bool CanCreateRepositoryLogEntry(string logEntryType, string logMessage, DateTime created)
		{
			return !string.IsNullOrWhiteSpace(logEntryType)
					&& !string.IsNullOrWhiteSpace(logMessage)
					&& created != DateTime.MinValue
					&& created != DateTime.MaxValue;
		}
	}
}

I’m following a similiar structure here as I had done in the LogEntryFactory class above. The CanCreateRepositoryLogEntry() method ensures required parameters are passed to methods on the class. If they are not, then a null reference is returned to the caller.

Since I hate hardcoding things, I decided to create a service class that gets the newline character. The following interface is for classes that do that:

namespace Sandbox.Foundation.Forms.Services.Environment
{
	public interface IEnvironmentService
	{
		string GetNewLine();
	}
}

This next class implements the interface above:

namespace Sandbox.Foundation.Forms.Services.Environment
{
	public class EnvironmentService : IEnvironmentService
	{
		public string GetNewLine()
		{
			return System.Environment.NewLine;
		}
	}
}

In the class above, I’m taking advantage of stuff build into the .NET library for getting the newline character.

I love when I discover things like this, albeit wish I had found something like this when trying to find an html break string for something I was working on the other day, but I digress (if you know of a way, please let me know in a comment below 😉 ).

The above interface and class might seem out of place in this post but I am using them when formatting messages for the LogEntry instances further down in another service class. Just keep an eye out for it.

Since I loathe hardcoding strings with a passion, I like to hide these away in Sitecore configuration patch files and hydrate a POCO class instance with the values from the aforementioned configuration. The following class is such a POCO settings object for a service class I will discuss further down in the post:

namespace Sandbox.Foundation.Forms.Models.Logging
{
	public class LogEntryServiceSettings
	{
		public string DebugLogEntryType { get; set; }

		public string ErrorLogEntryType { get; set; }

		public string FatalLogEntryType { get; set; }

		public string InfoLogEntryType { get; set; }

		public string WarnLogEntryType { get; set; }

		public string ExceptionPrefix { get; set; }

		public string MessagePrefix { get; set; }

		public string SourcePrefix { get; set; }

		public string NestedExceptionPrefix { get; set; }

		public string LogEntryTimeFormat { get; set; }
	}
}

Okay, so need we need to know what “type” of LogEntry we are dealing with — is it an error or a warning or what? — before sending to a repository to save in the database. I created the following interface for service classes that return back strings for the different LogEntry types, and also generate a log message from the data on properties on the LogEntry instance — this is the message that will end up in the database for the LogEntry:

using Sandbox.Foundation.Forms.Models.Logging;

namespace Sandbox.Foundation.Forms.Services.Diagnostics
{
	public interface ILogEntryService
	{
		string GetDebugLogEntryType();

		string GetErrorLogEntryType();

		string GetFatalLogEntryType();

		string GetInfoLogEntryType();

		string GetWarnLogEntryType();

		string GenerateLogMessage(LogEntry entry);
	}
}

And here is its implementation class:

using System;
using System.Text;

using Sandbox.Foundation.Forms.Models.Logging;
using Sandbox.Foundation.Forms.Services.Environment;

namespace Sandbox.Foundation.Forms.Services.Diagnostics
{
	public class LogEntryService : ILogEntryService
	{
		private readonly string _newLine;
		private readonly LogEntryServiceSettings _logEntryServiceSettings;

		public LogEntryService(IEnvironmentService environmentService, LogEntryServiceSettings logEntryServiceSettings)
		{
			_newLine = GetNewLine(environmentService);
			_logEntryServiceSettings = logEntryServiceSettings;
		}

		protected virtual string GetNewLine(IEnvironmentService environmentService)
		{
			return environmentService.GetNewLine();
		}

		public string GetDebugLogEntryType()
		{
			return _logEntryServiceSettings.DebugLogEntryType;
		}

		public string GetErrorLogEntryType()
		{
			return _logEntryServiceSettings.ErrorLogEntryType;
		}

		public string GetFatalLogEntryType()
		{
			return _logEntryServiceSettings.FatalLogEntryType;
		}

		public string GetInfoLogEntryType()
		{
			return _logEntryServiceSettings.InfoLogEntryType;
		}

		public string GetWarnLogEntryType()
		{
			return _logEntryServiceSettings.WarnLogEntryType;
		}

		public string GenerateLogMessage(LogEntry entry)
		{
			if(!CanGenerateLogMessage(entry))
			{
				return string.Empty;
			}

			string exceptionMessage = GenerateExceptionMessage(entry.Exception);
			if(string.IsNullOrWhiteSpace(exceptionMessage))
			{
				return $"{entry.Message}";
			}

			return $"{entry.Message} {exceptionMessage}";
		}

		protected virtual bool CanGenerateLogMessage(LogEntry entry)
		{
			return entry != null
					&& !string.IsNullOrWhiteSpace(entry.Message)
					&& entry.Owner != null;
		}

		protected virtual string GenerateExceptionMessage(Exception exception)
		{
			if(exception == null)
			{
				return string.Empty;
			}

			StringBuilder messageBuilder = new StringBuilder();
			messageBuilder.Append(_logEntryServiceSettings.ExceptionPrefix).Append(exception.GetType().FullName); ;
			AppendNewLine(messageBuilder);
			messageBuilder.Append(_logEntryServiceSettings.MessagePrefix).Append(exception.Message);
			AppendNewLine(messageBuilder);

			if (!string.IsNullOrWhiteSpace(exception.Source))
			{
				messageBuilder.Append(_logEntryServiceSettings.SourcePrefix).Append(exception.Source);
				AppendNewLine(messageBuilder);
			}

			if(!string.IsNullOrWhiteSpace(exception.StackTrace))
			{
				messageBuilder.Append(exception.StackTrace);
				AppendNewLine(messageBuilder);
			}
			
			if (exception.InnerException != null)
			{
				AppendNewLine(messageBuilder);
				messageBuilder.Append(_logEntryServiceSettings.NestedExceptionPrefix);
				AppendNewLine(messageBuilder, 3);
				messageBuilder.Append(GenerateExceptionMessage(exception.InnerException));
				AppendNewLine(messageBuilder);
			}
			
			return messageBuilder.ToString();
		}

		protected virtual void AppendNewLine(StringBuilder builder, int repeatCount = 1)
		{
			AppendRepeat(builder, _newLine, repeatCount);
		}

		protected virtual void AppendRepeat(StringBuilder builder, string stringToAppend, int repeatCount)
		{
			if (builder == null || string.IsNullOrWhiteSpace(stringToAppend) || repeatCount < 1)
			{
				return;
			}

			for(int i = 0; i < repeatCount; i++)
			{
				builder.Append(stringToAppend);
			}
		}
	}
}

I’m not going to discuss all the code in the above class as it should be self-explanatory.

I do want to point out GenerateLogMessage() will generate one of two strings, depending on whether an Exception was set on the LogEntry instance.

If an Exception was set, we append the Exception details — the GenerateExceptionMessage() method generates a string from the Exception — onto the end of the LogEntry message

If it was not set, we just return the LogEntry message to the caller.

Well, now we need a place to store the log entries. I used the following SQL script to create a new table for storing these:

USE [ExperienceFormsSettings]
GO

CREATE TABLE [dbo].[ExperienceFormsLog](
	[ID] [uniqueidentifier] NOT NULL,
	[LogEntryType] [nvarchar](max) NOT NULL,
	[LogMessage] [nvarchar](max) NOT NULL,
	[Created] [datetime] NOT NULL,
 CONSTRAINT [PK_ExperienceFormsLog] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

ALTER TABLE [dbo].[ExperienceFormsLog] ADD  DEFAULT (newsequentialid()) FOR [ID]
GO

I also sprinkled some magical database dust onto the table:

😉

Wonderful, we now can move on to the fun bit — actually writing some code to store these entries into the database table created from the SQL script above.

I wrote the following POCO class to represent a SQL command — either a query or statement (it really doesn’t matter as it will support both):

namespace Sandbox.Foundation.Forms.Models.Logging
{
	public class SqlCommand
	{
		public string Sql { get; set; }

		public object[] Parameters { get; set; }
	}
}

I’m sure I could have found something in Sitecore.Kernel.dll that does exactly what the class above does but I couldn’t find such a thing (if you know of such a class, please share in a comment below).

Now we need a settings class for the SQL Logger I am writing further down in this post. As I had done for the LogEntryService class above, this data will be coming from Sitecore configuration:

namespace Sandbox.Foundation.Forms.Models.Logging
{
	public class SqlLoggerSettings
	{
		public string LogPrefix { get; set; }

		public string LogDatabaseConnectionStringName { get; set; }

		public string InsertLogEntrySqlFormat { get; set; }

		public string ConnectionStringNameColumnName { get; set; }

		public string FieldsPrefixColumnName { get; set; }

		public string FieldsIndexNameColumnName { get; set; }

		public int NotFoundOrdinal { get; set; }

		public string LogEntryTypeParameterName { get; set; }

		public string LogMessageParameterName { get; set; }

		public string CreatedParameterName { get; set; }
	}
}

Now the fun part — creating an implementation of Sitecore.ExperienceForms.Diagnostics.ILogger:

using System;

using Sitecore.Abstractions;
using Sitecore.Data.DataProviders.Sql;
using Sitecore.ExperienceForms.Diagnostics;

using Sandbox.Foundation.Forms.Services.Factories;
using Sandbox.Foundation.Forms.Models.Logging;
using Sandbox.Foundation.Forms.Services.Factories.Diagnostics;

namespace Sandbox.Foundation.Forms.Services.Diagnostics
{
	public class SqlLogger : ILogger
	{
		private readonly SqlLoggerSettings _sqlLoggerSettings;
		private readonly BaseSettings _settings;
		private readonly BaseFactory _factory;
		private readonly SqlDataApi _sqlDataApi;
		private readonly ILogEntryFactory _logEntryFactory;
		private readonly ILogEntryService _logEntryService;
		private readonly IRepositoryLogEntryFactory _repositoryLogEntryFactory;

		public SqlLogger(SqlLoggerSettings sqlLoggerSettings, BaseSettings settings, BaseFactory factory, ISqlDataApiFactory sqlDataApiFactory, ILogEntryFactory logEntryFactory, IRepositoryLogEntryFactory repositoryLogEntryFactory, ILogEntryService logEntryService)
		{
			_sqlLoggerSettings = sqlLoggerSettings;
			_settings = settings;
			_factory = factory;
			_sqlDataApi = CreateSqlDataApi(sqlDataApiFactory);
			_logEntryFactory = logEntryFactory;
			_logEntryService = logEntryService;
			_repositoryLogEntryFactory = repositoryLogEntryFactory;
		}

		protected virtual SqlDataApi CreateSqlDataApi(ISqlDataApiFactory sqlDataApiFactory)
		{
			return sqlDataApiFactory.CreateSqlDataApi(GetLogDatabaseConnectionString());
		}

		protected virtual string GetLogDatabaseConnectionString()
		{
			return _settings.GetConnectionString(GetLogDatabaseConnectionStringName());
		}

		protected virtual string GetLogDatabaseConnectionStringName()
		{
			return _sqlLoggerSettings.LogDatabaseConnectionStringName;
		}

		public void Debug(string message)
		{
			Debug(message, GetDefaultOwner());
		}

		public void Debug(string message, object owner)
		{
			SaveLogEntry(CreateLogEntry(GetDebugLogEntryType(), message, owner, GetLogEntryDateTime()));
		}

		protected virtual string GetDebugLogEntryType()
		{
			return _logEntryService.GetDebugLogEntryType();
		}

		public void LogError(string message)
		{
			LogError(message, null, GetDefaultOwner());
		}

		public void LogError(string message, object owner)
		{
			LogError(message, null, owner);
		}

		public void LogError(string message, Exception exception, Type ownerType)
		{
			LogError(message, exception, (object)ownerType);
		}

		public void LogError(string message, Exception exception, object owner)
		{
			SaveLogEntry(CreateLogEntry(GetErrorLogEntryType(), message, exception, owner, GetLogEntryDateTime()));
		}

		protected virtual string GetErrorLogEntryType()
		{
			return _logEntryService.GetErrorLogEntryType();
		}

		public void Fatal(string message)
		{
			Fatal(message, null, GetDefaultOwner());
		}

		public void Fatal(string message, object owner)
		{
			Fatal(message, null, owner);
		}

		public void Fatal(string message, Exception exception, Type ownerType)
		{
			Fatal(message, exception, (object)ownerType);
		}

		public void Fatal(string message, Exception exception, object owner)
		{
			SaveLogEntry(CreateLogEntry(GetFatalLogEntryType(), message, exception, owner, GetLogEntryDateTime()));
		}

		protected virtual string GetFatalLogEntryType()
		{
			return _logEntryService.GetFatalLogEntryType();
		}

		public void Info(string message)
		{
			Info(message, GetDefaultOwner());
		}

		public void Info(string message, object owner)
		{
			SaveLogEntry(CreateLogEntry(GetInfoLogEntryType(), message, owner, GetLogEntryDateTime()));
		}

		protected virtual string GetInfoLogEntryType()
		{
			return _logEntryService.GetInfoLogEntryType();
		}

		public void Warn(string message)
		{
			Warn(message, GetDefaultOwner());
		}

		public void Warn(string message, object owner)
		{
			SaveLogEntry(CreateLogEntry(GetWarnLogEntryType(), message, owner, GetLogEntryDateTime()));
		}

		protected virtual string AddPrefixToMessage(string message)
		{
			return string.Concat(_sqlLoggerSettings.LogPrefix, message);
		}

		protected virtual object GetDefaultOwner()
		{
			return this;
		}

		protected virtual LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, Type ownerType, DateTime createdDate)
		{
			return _logEntryFactory.CreateLogEntry(logEntryType, message, exception, ownerType, createdDate);
		}

		protected virtual LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, object owner, DateTime createdDate)
		{
			return _logEntryFactory.CreateLogEntry(logEntryType, message, exception, owner, createdDate);
		}

		protected virtual LogEntry CreateLogEntry(string logEntryType, string message, Type ownerType, DateTime createdDate)
		{
			return _logEntryFactory.CreateLogEntry(logEntryType, message, ownerType, createdDate);
		}

		protected virtual LogEntry CreateLogEntry(string logEntryType, string message, object owner, DateTime createdDate)
		{
			return _logEntryFactory.CreateLogEntry(logEntryType, message, owner, createdDate);
		}

		protected virtual string GetWarnLogEntryType()
		{
			return _logEntryService.GetWarnLogEntryType();
		}

		protected virtual DateTime GetLogEntryDateTime()
		{
			return DateTime.Now.ToUniversalTime();
		}

		protected virtual void SaveLogEntry(LogEntry entry)
		{
			if (entry == null)
			{
				return;
			}

			entry.LogMessage = _logEntryService.GenerateLogMessage(entry);

			RepositoryLogEntry repositoryEntry = CreateRepositoryLogEntry(entry);
			if (repositoryEntry == null)
			{
				return;
			}

			SaveRepositoryLogEntry(repositoryEntry);
		}

		protected virtual string GenerateLogMessage(LogEntry entry)
		{
			return _logEntryService.GenerateLogMessage(entry);
		}

		protected virtual RepositoryLogEntry CreateRepositoryLogEntry(LogEntry entry)
		{
			return _repositoryLogEntryFactory.CreateRepositoryLogEntry(entry);
		}

		protected virtual void SaveRepositoryLogEntry(RepositoryLogEntry entry)
		{
			if(!CanLogEntry(entry))
			{
				return;
			}

			SqlCommand insertCommand = GetinsertCommand(entry);
			if(insertCommand == null)
			{
				return;
			}

			ExecuteNoResult(insertCommand);
		}

		protected virtual bool CanLogEntry(RepositoryLogEntry entry)
		{
			return entry != null
					&& !string.IsNullOrWhiteSpace(entry.LogEntryType)
					&& !string.IsNullOrWhiteSpace(entry.LogMessage)
					&& entry.Created > DateTime.MinValue
					&& entry.Created < DateTime.MaxValue;
		}

		protected virtual SqlCommand GetinsertCommand(RepositoryLogEntry entry)
		{
			return new SqlCommand
			{
				Sql = GetInsertLogEntrySql(),
				Parameters = GetinsertCommandParameters(entry)
			};
		}

		protected virtual object[] GetinsertCommandParameters(RepositoryLogEntry entry)
		{
			return new object[]
			{
				GetLogEntryTypeParameterName(),
				entry.LogEntryType,
				GetLogMessageParameterName(),
				entry.LogMessage,
				GetCreatedParameterName(),
				entry.Created
			};
		}

		protected virtual string GetLogEntryTypeParameterName()
		{
			return _sqlLoggerSettings.LogEntryTypeParameterName;
		}

		protected virtual string GetLogMessageParameterName()
		{
			return _sqlLoggerSettings.LogMessageParameterName;
		}

		protected virtual string GetCreatedParameterName()
		{
			return _sqlLoggerSettings.CreatedParameterName;
		}

		protected virtual string GetInsertLogEntrySql()
		{
			return _sqlLoggerSettings.InsertLogEntrySqlFormat;
		}

		protected virtual void ExecuteNoResult(SqlCommand sqlCommand)
		{
			_factory.GetRetryer().ExecuteNoResult(() => { _sqlDataApi.Execute(sqlCommand.Sql, sqlCommand.Parameters); });
		}
	}
}

Since there is a lot of code in the class above, I’m not going to talk about all of it — it should be clear on what this class is doing for the most part.

I do want to highlight that the SaveRepositoryLogEntry() method takes in a RepositoryLogEntry instance; builds up a SqlCommand instance from it as well as the insert SQL statement and parameters from the SqlLoggerSettings instance (these are coming from Sitecore configuration, and there are hooks on this class to allow for overriding these if needed); and passes the SqlCommand instance to the ExecuteNoResult() method which uses the SqlDataApi instance for saving to the database. Plus, I’m leveraging an “out of the box” “retryer” from the Sitecore.Kernel.dll to ensure it makes its way into the database table.

Moreover, I’m reusing the ISqlDataApiFactory instance above from my previous post. Have a read of it so you can see what this factory class does.

Since Experience Forms was built perfectly — 😉 — I couldn’t see any LogEntry instances being saved to my database right away. So went ahead and created some <forms.renderField> pipeline processors to capture some.

The following interface is for a <forms.renderField> pipeline processor to just throw an exception by dividing by zero:

using Sitecore.ExperienceForms.Mvc.Pipelines.RenderField;

namespace Sandbox.Foundation.Forms.Pipelines.RenderField
{
	public interface IThrowExceptionProcessor
	{
		void Process(RenderFieldEventArgs args);
	}
}

Here is its implementation class:

using System;

using Sitecore.ExperienceForms.Diagnostics;
using Sitecore.ExperienceForms.Mvc.Pipelines.RenderField;

namespace Sandbox.Foundation.Forms.Pipelines.RenderField
{
	public class ThrowExceptionProcessor : IThrowExceptionProcessor
	{
		private readonly ILogger _logger;

		public ThrowExceptionProcessor(ILogger logger)
		{
			_logger = logger;
		}

		public void Process(RenderFieldEventArgs args)
		{
			try
			{
				int i = 1 / GetZero();
			}
			catch(Exception ex)
			{
				_logger.LogError(ToString(), ex, this);
			}
		}

		private int GetZero()
		{
			return 0;
		}
	}
}

I’m sure you would never do such a thing, right? 😉

I then created the following interface for another <forms.renderField> pipeline processor to log some information on the RenderFieldEventArgs instance sent to the Process() method:

using Sitecore.ExperienceForms.Mvc.Pipelines.RenderField;

namespace Sandbox.Foundation.Forms.Pipelines.RenderField
{
	public interface ILogRenderedFieldInfo
	{
		void Process(RenderFieldEventArgs args);
	}
}

Here is the implementation class for this:

using Sitecore.ExperienceForms.Diagnostics;
using Sitecore.ExperienceForms.Mvc.Pipelines.RenderField;
using Sitecore.Mvc.Pipelines;

namespace Sandbox.Foundation.Forms.Pipelines.RenderField
{
	public class LogRenderedFieldInfo : MvcPipelineProcessor<RenderFieldEventArgs>, ILogRenderedFieldInfo
	{
		private readonly ILogger _logger;

		public LogRenderedFieldInfo(ILogger logger)
		{
			_logger = logger;
		}

		public override void Process(RenderFieldEventArgs args)
		{
			LogInfo($"ViewModel Details:\n\nName: {args.ViewModel.Name}, ItemId: {args.ViewModel.ItemId}, TemplateId: {args.ViewModel.TemplateId}, FieldTypeItemId: {args.ViewModel.FieldTypeItemId}");
			LogInfo($"RenderingSettings Details\n\nFieldTypeName: {args.RenderingSettings.FieldTypeName}, FieldTypeId: {args.RenderingSettings.FieldTypeId}, FieldTypeIcon: {args.RenderingSettings.FieldTypeIcon}, FieldTypeDisplayName: {args.RenderingSettings.FieldTypeDisplayName}, FieldTypeBackgroundColor: {args.RenderingSettings.FieldTypeBackgroundColor}");
			LogInfo($"Item Details: {args.Item.ID}, Name: {args.Item.Name} FullPath: {args.Item.Paths.FullPath}, TemplateID: {args.Item.TemplateID}");
		}

		protected virtual void LogInfo(string message)
		{
			if(string.IsNullOrWhiteSpace(message))
			{
				return;
			}

			_logger.Info(message);
		}
	}
}

I then registered everything in the Sitecore IoC container using the following configurator:

using System;

using Microsoft.Extensions.DependencyInjection;

using Sitecore.Abstractions;
using Sitecore.DependencyInjection;
using Sitecore.ExperienceForms.Diagnostics;

using Sandbox.Foundation.Forms.Services.Factories.Diagnostics;
using Sandbox.Foundation.Forms.Services.Factories;
using Sandbox.Foundation.Forms.Models.Logging;
using Sandbox.Foundation.Forms.Services.Environment;
using Sandbox.Foundation.Forms.Services.Diagnostics;
using Sandbox.Foundation.Forms.Pipelines.RenderField;

namespace Sandbox.Foundation.Forms
{
	public class SqlLoggerConfigurator : IServicesConfigurator
	{
		public void Configure(IServiceCollection serviceCollection)
		{
			ConfigureConfigObjects(serviceCollection);
			ConfigureFactories(serviceCollection);
			ConfigureServices(serviceCollection);
			ConfigurePipelineProcessors(serviceCollection);
		}

		private void ConfigureConfigObjects(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton(provider => GetLogEntryServiceSettings(provider));
			serviceCollection.AddSingleton(provider => GetSqlLoggerSettings(provider));
		}

		private LogEntryServiceSettings GetLogEntryServiceSettings(IServiceProvider provider)
		{
			return CreateConfigObject<LogEntryServiceSettings>(provider, "moduleSettings/foundation/forms/logEntryServiceSettings");
		}

		private SqlLoggerSettings GetSqlLoggerSettings(IServiceProvider provider)
		{
			return CreateConfigObject<SqlLoggerSettings>(provider, "moduleSettings/foundation/forms/sqlLoggerSettings");
		}

		private TConfigObject CreateConfigObject<TConfigObject>(IServiceProvider provider, string path) where TConfigObject : class
		{
			BaseFactory factory = GetService<BaseFactory>(provider);
			return factory.CreateObject(path, true) as TConfigObject;
		}

		private TService GetService<TService>(IServiceProvider provider)
		{
			return provider.GetService<TService>();
		}

		private void ConfigureFactories(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<ILogEntryFactory, LogEntryFactory>();
			serviceCollection.AddSingleton<IRepositoryLogEntryFactory, RepositoryLogEntryFactory>();
			serviceCollection.AddSingleton<ISqlDataApiFactory, SqlDataApiFactory>();
		}

		private void ConfigureServices(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<IEnvironmentService, EnvironmentService>();
			serviceCollection.AddSingleton<ILogEntryService, LogEntryService>();
			serviceCollection.AddSingleton<ILogger, SqlLogger>();
		}

		private void ConfigurePipelineProcessors(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<ILogRenderedFieldInfo, LogRenderedFieldInfo>();
			serviceCollection.AddSingleton<IThrowExceptionProcessor, ThrowExceptionProcessor>();
		}
	}
}

Note: the GetLogEntryServiceSettings() and the GetSqlLoggerSettings() methods both create settings objects by using the Sitecore Configuration Factory. Ultimately, these settings objects are thrown into the container so they can be injected into the service classes that need them.

I then strung everything together using the following the Sitecore patch configuration file.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<pipelines>
			<forms.renderField>
				<processor type="Sandbox.Foundation.Forms.Pipelines.RenderField.LogRenderedFieldInfo, Sandbox.Foundation.Forms" resolve="true"/>
				<processor type="Sandbox.Foundation.Forms.Pipelines.RenderField.ThrowExceptionProcessor, Sandbox.Foundation.Forms" resolve="true"/>
			</forms.renderField>
		</pipelines>
		<services>
			<configurator type="Sandbox.Foundation.Forms.SqlLoggerConfigurator, Sandbox.Foundation.Forms" />
			<register serviceType="Sitecore.ExperienceForms.Diagnostics.ILogger, Sitecore.ExperienceForms">
				<patch:delete />
			</register>
		</services>

		<moduleSettings>
			<foundation>
				<forms>
					<logEntryServiceSettings type="Sandbox.Foundation.Forms.Models.Logging.LogEntryServiceSettings, Sandbox.Foundation.Forms" singleInstance="true">
						<DebugLogEntryType>DEBUG</DebugLogEntryType>
						<ErrorLogEntryType>ERROR</ErrorLogEntryType>
						<FatalLogEntryType>FATAL</FatalLogEntryType>
						<InfoLogEntryType>INFO</InfoLogEntryType>
						<WarnLogEntryType>WARN</WarnLogEntryType>
						<ExceptionPrefix>Exception: </ExceptionPrefix>
						<MessagePrefix>Message: </MessagePrefix>
						<SourcePrefix>Source: </SourcePrefix>
						<NestedExceptionPrefix>Nested Exception</NestedExceptionPrefix>
						<LogEntryTimeFormat>HH:mm:ss.ff</LogEntryTimeFormat>
					</logEntryServiceSettings>
					<sqlLoggerSettings type="Sandbox.Foundation.Forms.Models.Logging.SqlLoggerSettings, Sandbox.Foundation.Forms" singleInstance="true">
						<LogPrefix>[Experience Forms]:</LogPrefix>
						<LogDatabaseConnectionStringName>ExperienceFormsSettings</LogDatabaseConnectionStringName>
						<InsertLogEntrySqlFormat>INSERT INTO {0}ExperienceFormsLog{1}({0}LogEntryType{1},{0}LogMessage{1},{0}Created{1})VALUES({2}logEntryType{3},{2}logMessage{3},{2}created{3});</InsertLogEntrySqlFormat>
						<LogEntryTypeParameterName>logEntryType</LogEntryTypeParameterName>
						<LogMessageParameterName>logMessage</LogMessageParameterName>
						<CreatedParameterName>created</CreatedParameterName>
					</sqlLoggerSettings>
				</forms>
			</foundation>
		</moduleSettings>
	</sitecore>
</configuration> 

Ok, let’s take this for a spin.

After building and deploying everything above, I spun up my Sitecore instance:

I then navigated to a form I had created in a previous post:

After the page with my form was done loading, I ran a query on my custom log table and saw this:

As you can see, it worked.

If you have any questions or comments, don’t hesitate to drop these in a comment below.

Until next time, have yourself a Sitecoretastic day!