Home » Events
Category Archives: Events
Restart Your Sitecore Content Management (CM) and Content Delivery (CD) Instances using Custom Events
Last Friday, I had worked on a PowerShell script which is executed by clicking a custom button in the Sitecore Content Editor ribbon using the Content Editor Ribbon integration point offered through Sitecore PowerShell Extensions (SPE) in a UAT environment. This UAT environment has separate Content Management (CM) and Content Delivery (CD) Sitecore instances on two different servers. The script updates some records in an external datasource (data not in Sitecore) but its data is cached in the Sitecore instances outside of the stand Sitecore caching framework. In order to see these updates on the sites which live on these Sitecore instances , I needed a programmatic way to restart all Sitecore instances in the UAT environment I had built this solution for, and the following solution reflects what I had worked on to accomplish this spanning Friday until today — this post defines two custom events in Sitecore — just so I could get this out as soon as possible as it may help you understand custom events, and maybe even help you do something similar to what I am doing here.
I do want to caution you on using code as follows in a production environment — this could cause disruption to your production CDs which are live to the public; be sure to take your CD servers out of a load-balancer before restarting the Sitecore instance on them as you don’t want people showing up to your door with pitchforks and torches asking why production had been down for a bit.
I’ve also incorporated the concept of Sitecore configuration-driven feature toggles which I had discussed in a previous blog post where I had altered the appearance of the Sitecore content tree using a pipeline-backed MasterDataView, and will be repurposing code from that post here — I strongly suggest reading that post to understand the approach being used for this post as I will not discuss much how this works on this post.
Moreover, I have used numerous Configuration Objects sourced from the Sitecore IoC container in this post which I had discuss in another previous post; you might also want to have a read of that post before proceeding in order to understand why I am decorating some classes on this post with the [ServiceConfigObject] attribute.
I first defined the following Configuration Object which will live in the Sitecore IoC container:
using Foundation.DependencyInjection; using Foundation.DependencyInjection.Enums; using Foundation.Kernel.Services.FeatureToggle; namespace Foundation.Kernel.Models.RestartServer { [ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/restartServerSettings", Lifetime = Lifetime.Singleton)] public class RestartServerSettings : IFeatureToggleable { public bool Enabled { get; set; } } }
This Configuration Object will act is the main feature toggle to turn the entire feature on/off based on the Enabled property’s value from Sitecore configuration (see the Sitecore patch configuration file towards the bottom of this post).
I then created the following interface which will ascertain when the entire feature should be turned on/off at a global level, or at an individual piece of functionality’s level:
using Foundation.Kernel.Services.FeatureToggle; namespace Foundation.Kernel.Services.RestartServer { public interface IRestartServerFeatureToggleService { bool IsFeatureEnabled(); bool IsEnabled(IFeatureToggleable toggleable); } }
The following is the implementation of the interface above.
using Foundation.Kernel.Models.RestartServer; using Foundation.Kernel.Services.FeatureToggle; namespace Foundation.Kernel.Services.RestartServer { public class RestartServerFeatureToggleService : IRestartServerFeatureToggleService { private readonly RestartServerSettings _settings; private readonly IFeatureToggleService _featureToggleService; public RestartServerFeatureToggleService(RestartServerSettings settings, IFeatureToggleService featureToggleService) { _settings = settings; _featureToggleService = featureToggleService; } public bool IsEnabled(IFeatureToggleable toggleable) => _featureToggleService.IsEnabled(_settings, toggleable); public bool IsFeatureEnabled() => _featureToggleService.IsEnabled(_settings); } }
I won’t explain much here as I had talked about feature toggles on a previous configuration-driven post; I suggest reading that to understand this pattern.
Now that we have our feature toggle magic defined above, let’s dive into the code which defines and handles custom events which do the actual restarting of the Sitecore instances.
I created the following Config Object to hold log messaging formatted strings which we will use when writing to the Sitecore log when restarting Sitecore instances:
using Foundation.DependencyInjection; using Foundation.DependencyInjection.Enums; namespace Foundation.Kernel.Models.RestartServer.Events { [ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/restartServerEventHandlerSettings", Lifetime = Lifetime.Singleton)] public class RestartServerEventHandlerSettings { public string RestartServerLogMessageFormat { get; set; } public string RestartServerRemoteLogMessageFormat { get; set; } } }
We need a way to identify the Sitecore instance we are on in order to determine if the Sitecore instance needs to be restarted, and also when logging information that a restart is happening on that instance. The following Config Object will provide the name of a Sitecore setting which holds thie identifier for the Sitecore instance we are on:
using Foundation.DependencyInjection; using Foundation.DependencyInjection.Enums; namespace Foundation.Kernel.Models.Server { [ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/serverServiceSettings", Lifetime = Lifetime.Singleton)] public class ServerServiceSettings { public string InstanceNameSetting { get; set; } } }
The following interface/implementation class will provide the name of the Sitecore instance we are on:
namespace Foundation.Kernel.Services.Server { public interface IServerService { string GetCurrentServerName(); } }
Here’s the implementation of the interface above:
using Sitecore.Abstractions; using Foundation.Kernel.Models.Server; namespace Foundation.Kernel.Services.Server { public class ServerService : IServerService { private readonly ServerServiceSettings _settings; private readonly BaseSettings _settingsProvider; public ServerService(ServerServiceSettings settings, BaseSettings settingsProvider) { _settings = settings; _settingsProvider = settingsProvider; } public string GetCurrentServerName() => GetSetting(GetInstanceNameSetting()); protected virtual string GetInstanceNameSetting() => _settings.InstanceNameSetting; protected virtual string GetSetting(string settingName) => _settingsProvider.GetSetting(settingName); } }
The class above consumes the ServerServiceSettings config object then uses the BaseSettings service to get the name of the Sitecore instance based on the value in the setting, and returns it to the caller. There’s really nothing more to it.
Back in 2014, I had written a post on restarting your Sitecore instance using a custom FileWatcher, and in that post I had used the RestartServer() method on the Sitecore.Install.Installer class in Sitecore.Kernel. I will be using this same class but backed by a service class. Here is the interface of that service class:
namespace Foundation.Kernel.Services.Installer { public interface IInstallerService { void RestartServer(); } }
The following implements the interface above:
namespace Foundation.Kernel.Services.Installer { public class InstallerService : IInstallerService { public void RestartServer() => Sitecore.Install.Installer.RestartServer(); } }
The class above just calls the RestartServer() method on the Sitecore.Install.Installer class; ultimately this just does a “touch” on your Web.config to recycle your Sitecore instance.
Like all good Sitecore custom events, we need a custom EventArgs class — well, you don’t really need one if you aren’t passing any data beyond what lives on the System.EventArgs class but I’m sure you will seldom come across such a scenario 😉 — so I defined the following class to be just that class:
using System; using System.Collections.Generic; using Sitecore.Events; namespace Foundation.Kernel.Models.RestartServer.Events { [Serializable] public class RestartServerEventArgs : EventArgs, IPassNativeEventArgs { public List<string> ServerNames { get; protected set; } public RestartServerEventArgs(List<string> serverNames) { ServerNames = serverNames; } } }
We will be passing a List of server names just in case there are multiple servers we would like to reboot simultaneously.
Now it’s time to create the event handlers. I created the following abstract class for my two event handler classes to have a centralized place for most of this logic:
using System; using System.Linq; using Sitecore.Abstractions; using Foundation.Kernel.Models.RestartServer.Events; using Foundation.Kernel.Services.Installer; using Foundation.Kernel.Services.Server; using Foundation.Kernel.Services.RestartServer; using Foundation.Kernel.Services.FeatureToggle; namespace Foundation.Kernel.Events.RestartServer { public abstract class BaseRestartServerEventHandler : IFeatureToggleable { private readonly RestartServerEventHandlerSettings _settings; private readonly IRestartServerFeatureToggleService _restartServerFeatureToggleService; private readonly IServerService _serverService; private readonly IInstallerService _installerService; private readonly BaseLog _log; public bool Enabled { get; set; } public BaseRestartServerEventHandler(RestartServerEventHandlerSettings settings, IRestartServerFeatureToggleService restartServerFeatureToggleService, IServerService serverService, IInstallerService installerService, BaseLog log) { _settings = settings; _restartServerFeatureToggleService = restartServerFeatureToggleService; _serverService = serverService; _installerService = installerService; _log = log; } protected void ExecuteRestart(object sender, EventArgs args) { if(!IsEnabled()) { return; } RestartServerEventArgs restartServerArgs = GetArguments<RestartServerEventArgs>(args); if (!ShouldRestartServer(restartServerArgs)) { return; } string restartLogMessageFormat = GetRestartLogMessageFormat(); if (!string.IsNullOrWhiteSpace(restartLogMessageFormat)) { LogInfo(GetRestartServerMessage(restartLogMessageFormat, GetCurrentServerName())); } RestartServer(); } protected virtual bool IsEnabled() => _restartServerFeatureToggleService.IsEnabled(this); protected abstract string GetRestartLogMessageFormat(); protected virtual TArgs GetArguments<TArgs>(EventArgs args) where TArgs : EventArgs => args as TArgs; protected virtual bool ShouldRestartServer(RestartServerEventArgs restartServerArgs) { if(restartServerArgs.ServerNames == null || !restartServerArgs.ServerNames.Any()) { return false; } string currentServerName = GetCurrentServerName(); if (string.IsNullOrWhiteSpace(currentServerName)) { return false; } return restartServerArgs.ServerNames.Any(serverName => string.Equals(serverName, currentServerName, StringComparison.OrdinalIgnoreCase)); } protected virtual string GetCurrentServerName() => _serverService.GetCurrentServerName(); protected virtual string GetRestartServerMessage(string messageFormat, string serverName) => string.Format(messageFormat, serverName); protected virtual void RestartServer() => _installerService.RestartServer(); protected virtual void LogInfo(string message) => _log.Info(message, this); } }
Subclasses of the class above will have methods to handle their events which will delegate to the ExecuteRestart() method. This method will use the feature toggle service class to determine if it should execute or not; if not, it just gracefully exits.
If it does proceed forward, the passed System.EventArgs class is casted as RestartServerEventArgs instance, and the current Sitecore instance’s name is checked against the collection server names in the RestartServerEventArgs instance. If the current server’s name is in the list, the server is restarted though proceeded by a log message indicating that the server is being restarted.
Subclasses of thie abstract class above must provide the log message formatted string so the log message can be written to the Sitecore log.
I then created the following interface for an event handler class to handle a local server restart:
using System; namespace Foundation.Kernel.Events.RestartServer { public interface IRestartLocalServerEventHandler { void OnRestartTriggered(object sender, EventArgs args); } }
Here’s the implementation of the interface above:
using System; using Sitecore.Abstractions; using Foundation.Kernel.Models.RestartServer.Events; using Foundation.Kernel.Services.Installer; using Foundation.Kernel.Services.Server; using Foundation.Kernel.Services.RestartServer; namespace Foundation.Kernel.Events.RestartServer { public class RestartLocalServerEventHandler : BaseRestartServerEventHandler, IRestartLocalServerEventHandler { private readonly RestartServerEventHandlerSettings _settings; public RestartLocalServerEventHandler(RestartServerEventHandlerSettings settings, IRestartServerFeatureToggleService restartServerFeatureToggleService, IServerService serverService, IInstallerService installerService, BaseLog log) : base(settings, restartServerFeatureToggleService, serverService, installerService, log) { _settings = settings; } public void OnRestartTriggered(object sender, EventArgs args) => ExecuteRestart(sender, args); protected override string GetRestartLogMessageFormat() => _settings.RestartServerLogMessageFormat; } }
This subclass of the abstract class defined above just supplies its own log formatted message, and the rest of the logic is handled by its parent class.
I then created the following interface of an event handler class to remote events (those that run on CD servers):
using System; namespace Foundation.Kernel.Events.RestartServer { public interface IRestartRemoteServerEventHandler { void OnRestartTriggeredRemote(object sender, EventArgs args); } }
The following class implements the interface above:
using System; using Sitecore.Abstractions; using Foundation.Kernel.Models.RestartServer.Events; using Foundation.Kernel.Services.Installer; using Foundation.Kernel.Services.Server; using Foundation.Kernel.Services.RestartServer; namespace Foundation.Kernel.Events.RestartServer { public class RestartRemoteServerEventHandler : BaseRestartServerEventHandler, IRestartRemoteServerEventHandler { private readonly RestartServerEventHandlerSettings _settings; public RestartRemoteServerEventHandler(RestartServerEventHandlerSettings settings, IRestartServerFeatureToggleService restartServerFeatureToggleService, IServerService serverService, IInstallerService installerService, BaseLog log) : base(settings, restartServerFeatureToggleService, serverService, installerService, log) { _settings = settings; } public void OnRestartTriggeredRemote(object sender, EventArgs args) => ExecuteRestart(sender, args); protected override string GetRestartLogMessageFormat() => _settings.RestartServerRemoteLogMessageFormat; } }
Just as the previous event handler class had done, this class inherits from the abstract class defined above but just defines the GetRestartLogMessageFormat() method to provide its own log message formatted string; we need to identify that this was executed on a CD server in the Sitecore log.
In order to get remote events to work on your CD instances, you need to subscribe to your remote event on your CD server — this is done by listening for an instance of your custom EventArgs, we are using RestartServerEventArgs here, being sent across via the EventQueue — and then raise your custom remote event on our CD instance so your handlers are executed. The following code will be wrappers around static classes in the Sitecore API so I can use Dependency Injection in a custom <initialize> pipeline processor which ties all of this together with these injected service classes.
I will need to call methods on Sitecore.Events.Event in Sitecore.Kernel. I created the following interface/implementation for it:
using System; using Sitecore.Events; namespace Foundation.Kernel.Services.Events { public interface IEventService { TParameter ExtractParameter<TParameter>(EventArgs args, int index) where TParameter : class; object ExtractParameter(EventArgs args, int index); void RaiseEvent(string eventName, IPassNativeEventArgs args); EventResult RaiseEvent(string eventName, params object[] parameters); } }
using System; using Sitecore.Events; namespace Foundation.Kernel.Services.Events { public class EventService : IEventService { public TParameter ExtractParameter<TParameter>(EventArgs args, int index) where TParameter : class => Event.ExtractParameter<TParameter>(args, index); public object ExtractParameter(EventArgs args, int index) => Event.ExtractParameter(args, index); public void RaiseEvent(string eventName, IPassNativeEventArgs args) => Event.RaiseEvent(eventName, args); public EventResult RaiseEvent(string eventName, params object[] parameters) => Event.RaiseEvent(eventName, parameters); } }
I then created the following Config Object to provide the names of the two custom events (check out the patch configuration file at the end of this post to see what these values are):
using Foundation.DependencyInjection; using Foundation.DependencyInjection.Enums; namespace Foundation.Kernel.Models.RestartServer.Events { [ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/restartServerEventSettings", Lifetime = Lifetime.Singleton)] public class RestartServerEventSettings { public string RestartCurrentServerEventName { get; set; } public string RestartRemoteServerEventName { get; set; } } }
Now, I need a way to raise events, both on a local Sitecore instance but also on a remote instance. I defined the following interface for such a service class:
namespace Foundation.Kernel.Services.Events { public interface IEventTriggerer { void TriggerEvent(string eventName, params object[] parameters); void TriggerRemoteEvent<TEvent>(TEvent evt); void TriggerRemoteEvent<TEvent>(TEvent evt, bool triggerGlobally, bool triggerLocally); } }
I then implemented the interface above:
using Sitecore.Abstractions; namespace Foundation.Kernel.Services.Events { public class EventTriggerer : IEventTriggerer { private readonly IEventService _eventService; private readonly BaseEventQueueProvider _eventQueueProvider; public EventTriggerer(IEventService eventService, BaseEventQueueProvider eventQueueProvider) { _eventService = eventService; _eventQueueProvider = eventQueueProvider; } public void TriggerEvent(string eventName, params object[] parameters) => _eventService.RaiseEvent(eventName, parameters); public void TriggerRemoteEvent<TEvent>(TEvent evt) => _eventQueueProvider.QueueEvent(evt); public void TriggerRemoteEvent<TEvent>(TEvent evt, bool triggerGlobally, bool triggerLocally) => _eventQueueProvider.QueueEvent(evt, triggerGlobally, triggerLocally); } }
The class above consumes the IEventService service class we defined above so that we can “trigger”/raise local events on the current Sitecore instance, and it also consumes the BaseEventQueueProvider service which comes with stock Sitecore so we can “trigger”/raise events to run on remote servers via the EventQueue.
I also need to use methods on the Sitecore.Eventing.EventManager class in Sitecore.Kernel. The following interface/implementation do just that:
using System; using Sitecore.Eventing; namespace Foundation.Kernel.Services.Events { public interface IEventManagerService { SubscriptionId Subscribe<TEvent>(Action<TEvent> eventHandler); } }
using System; using Sitecore.Eventing; namespace Foundation.Kernel.Services.Events { public class EventManagerService : IEventManagerService { public SubscriptionId Subscribe<TEvent>(Action<TEvent> eventHandler) => EventManager.Subscribe(eventHandler); } }
I do want to call out that there is an underlying service behind the Sitecore.Eventing.EventManager class in Sitecore.Kernel but it’s brought into this static class through some service locator method I had never seen before, and was afraid of traversing too much down this rabbit hole out of fear of sticking my hands into a hornets nest, or even worse, a nest of those murderer hornets we keep hearing about on the news these days; I felt it was best not to go much further into this today. 😉
We can now dive into the <initialize> pipeline processor which binds the “subscribing and trigger remote event” functionality together.
I first created the following interface for the pipeline processor:
using Sitecore.Pipelines; namespace Foundation.Kernel.Pipelines.Initialize.RestartServer { public interface IRestartServerSubscriber { void Process(PipelineArgs args); } }
I then implemented the interface above:
using System; using Sitecore.Eventing; using Sitecore.Pipelines; using Foundation.Kernel.Services.Events; using Foundation.Kernel.Services.FeatureToggle; using Foundation.Kernel.Services.RestartServer; using Foundation.Kernel.Models.RestartServer.Events; namespace Foundation.Kernel.Pipelines.Initialize.RestartServer { public class RestartServerSubscriber: IFeatureToggleable, IRestartServerSubscriber { private readonly RestartServerEventSettings _settings; private readonly IRestartServerFeatureToggleService _restartServerFeatureToggleService; private readonly IEventTriggerer _eventTriggerer; private readonly IEventManagerService _eventManagerService; public bool Enabled { get; set; } public RestartServerSubscriber(RestartServerEventSettings settings, IRestartServerFeatureToggleService restartServerFeatureToggleService, IEventTriggerer eventTriggerer, IEventManagerService eventManagerService) { _settings = settings; _restartServerFeatureToggleService = restartServerFeatureToggleService; _eventTriggerer = eventTriggerer; _eventManagerService = eventManagerService; } public void Process(PipelineArgs args) { if(!IsEnabled()) { return; } Subscribe(GetRaiseRestartServerEventMethod()); } protected virtual bool IsEnabled() => _restartServerFeatureToggleService.IsEnabled(this); protected virtual Action<RestartServerEventArgs> GetRaiseRestartServerEventMethod() => new Action<RestartServerEventArgs>(RaiseRestartServerEvent); protected virtual void RaiseRestartServerEvent(RestartServerEventArgs args) => RaiseEvent(GetRestartRemoteServerEventName(), args); protected virtual string GetRestartRemoteServerEventName() => _settings.RestartRemoteServerEventName; protected virtual void RaiseEvent(string eventName, params object[] parameters) => _eventTriggerer.TriggerEvent(eventName, parameters); protected virtual SubscriptionId Subscribe<TEvent>(Action<TEvent> eventHandler) => _eventManagerService.Subscribe(eventHandler); } }
The Process() method of the pipeline processor class above uses the feature toggle service we had discuss earlier in this post to determine if it should execute or not.
If it can execute, it will subscribe to the remote event by listening for an RestartServerEventArgs instance being sent across by the EventQueue.
If it one is sent across, it will “trigger” (raise) the custom remote event on the CD server this pipeline processor lives on.
Are you still with me? 😉
Now we need a service class which glues all of the stuff above into a nice simple API which we can call. I defined the following Config Object which will be consumed by this service class; this Config Object determines if Remote events should also run on local servers — I had set this up so I can turn this on for testing/troubleshooting/debugging on my local development instance:
using Foundation.DependencyInjection; using Foundation.DependencyInjection.Enums; namespace Foundation.Kernel.Models.RestartServer.Events { [ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/restartServerRemoteEventSettings", Lifetime = Lifetime.Singleton)] public class RestartServerRemoteEventSettings { public bool TriggerRemoteEventLocally { get; set; } public bool TriggerRemoteEventGlobally { get; set; } } }
The following interface is for our “simple” API for restarting Sitecore instances:
using System.Collections.Generic; namespace Foundation.Kernel.Services.RestartServer { public interface IRestartServerService { void RestartCurrentServer(); void RestartRemoteServer(string serverName); void RestartRemoteServers(List<string> serverNames); } }
Here’s the implementation of the interface above:
using System.Collections.Generic; using System.Linq; using Foundation.Kernel.Models.RestartServer.Events; using Foundation.Kernel.Services.Events; using Foundation.Kernel.Services.Server; namespace Foundation.Kernel.Services.RestartServer { public class RestartServerService : IRestartServerService { private readonly RestartServerEventSettings _eventSettings; private readonly RestartServerRemoteEventSettings _remoteEventSettings; private readonly IRestartServerFeatureToggleService _restartServerFeatureToggleService; private readonly IServerService _serverService; private readonly IEventTriggerer _eventTriggerer; public RestartServerService(RestartServerEventSettings eventSettings, RestartServerRemoteEventSettings remoteEventSettings, IRestartServerFeatureToggleService restartServerFeatureToggleService, IServerService serverService, IEventTriggerer eventTriggerer) { _eventSettings = eventSettings; _remoteEventSettings = remoteEventSettings; _restartServerFeatureToggleService = restartServerFeatureToggleService; _serverService = serverService; _eventTriggerer = eventTriggerer; } public void RestartCurrentServer() { if(!IsFeatureEnabled()) { return; } TriggerEvent(GetRestartCurrentServerEventName(), CreateRestartServerEventArgs(CreateList(GetCurrentServerName()))); } protected virtual string GetRestartCurrentServerEventName() => _eventSettings.RestartCurrentServerEventName; protected virtual string GetCurrentServerName() => _serverService.GetCurrentServerName(); public void RestartRemoteServer(string serverName) => RestartRemoteServers(CreateList(serverName)); protected virtual List<string> CreateList(params string[] values) => values?.ToList(); public void RestartRemoteServers(List<string> serverNames) { if (!IsFeatureEnabled()) { return; } TriggerRemoteEvent(CreateRestartServerEventArgs(serverNames)); } protected virtual bool IsFeatureEnabled() => _restartServerFeatureToggleService.IsFeatureEnabled(); protected virtual RestartServerEventArgs CreateRestartServerEventArgs(List<string> serverNames) => new RestartServerEventArgs(serverNames); protected virtual void TriggerEvent(string eventName, params object[] parameters) => _eventTriggerer.TriggerEvent(eventName, parameters); protected virtual void TriggerRemoteEvent<TEvent>(TEvent evt) => _eventTriggerer.TriggerRemoteEvent(evt, GetTriggerRemoteEventGlobally(), GetTriggerRemoteEventGlobally()); protected virtual bool GetTriggerRemoteEventGlobally() => _remoteEventSettings.TriggerRemoteEventGlobally; protected virtual bool GetTriggerRemoteEventLocally() => _remoteEventSettings.TriggerRemoteEventLocally; } }
The class above implements methods for restarting the current server, one remote server, or multiple remote servers — ultimately, it just delegates to the IEventTriggerer service class we defined further above, and uses other services discussed earlier in this post; there’s really not much to it.
I then stitched everything above together in the following Sitecore patch configuration file:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/"> <sitecore> <events> <event name="server:restart"> <handler type="Foundation.Kernel.Events.RestartServer.IRestartLocalServerEventHandler, Foundation.Kernel" method="OnRestartTriggered" resolve="true"> <Enabled>true</Enabled> </handler> </event> <event name="server:restart:remote"> <handler type="Foundation.Kernel.Events.RestartServer.IRestartRemoteServerEventHandler, Foundation.Kernel" method="OnRestartTriggeredRemote" resolve="true"> <Enabled>true</Enabled> </handler> </event> </events> <pipelines> <initialize> <processor type="Foundation.Kernel.Pipelines.Initialize.RestartServer.IRestartServerSubscriber, Foundation.Kernel" resolve="true"> <Enabled>true</Enabled> </processor> </initialize> </pipelines> <moduleSettings> <foundation> <kernel> <serverServiceSettings type="Foundation.Kernel.Models.Server.ServerServiceSettings, Foundation.Kernel" singleInstance="true"> <InstanceNameSetting>InstanceName</InstanceNameSetting> </serverServiceSettings> <restartServerSettings type="Foundation.Kernel.Models.RestartServer.RestartServerSettings, Foundation.Kernel" singleInstance="true"> <Enabled>true</Enabled> </restartServerSettings> <restartServerEventHandlerSettings type="Foundation.Kernel.Models.RestartServer.Events.RestartServerEventHandlerSettings, Foundation.Kernel" singleInstance="true"> <RestartServerLogMessageFormat>Restart Server Event Triggered: Shutting down server {0}</RestartServerLogMessageFormat> <RestartServerRemoteLogMessageFormat>Restart Server Remote Event Triggered: Shutting down server {0}</RestartServerRemoteLogMessageFormat> </restartServerEventHandlerSettings> <restartServerEventSettings type="Foundation.Kernel.Models.RestartServer.Events.RestartServerEventSettings, Foundation.Kernel" singleInstance="true"> <RestartCurrentServerEventName>server:restart</RestartCurrentServerEventName> <RestartRemoteServerEventName>server:restart:remote</RestartRemoteServerEventName> </restartServerEventSettings> <restartServerRemoteEventSettings type="Foundation.Kernel.Models.RestartServer.Events.RestartServerRemoteEventSettings, Foundation.Kernel" singleInstance="true"> <!-- setting this to "true" so I can test/debug locally --> <TriggerRemoteEventLocally>true</TriggerRemoteEventLocally> <TriggerRemoteEventGlobally>true</TriggerRemoteEventGlobally> </restartServerRemoteEventSettings> </kernel> </foundation> </moduleSettings> <services> <!-- General Services --> <register serviceType="Foundation.Kernel.Services.Installer.IInstallerService, Foundation.Kernel" implementationType="Foundation.Kernel.Services.Installer.InstallerService, Foundation.Kernel" lifetime="Singleton" /> <register serviceType="Foundation.Kernel.Services.Server.IServerService, Foundation.Kernel" implementationType="Foundation.Kernel.Services.Server.ServerService, Foundation.Kernel" lifetime="Singleton" /> <register serviceType="Foundation.Kernel.Services.RestartServer.IRestartServerFeatureToggleService, Foundation.Kernel" implementationType="Foundation.Kernel.Services.RestartServer.RestartServerFeatureToggleService, Foundation.Kernel" lifetime="Singleton" /> <register serviceType="Foundation.Kernel.Services.RestartServer.IRestartServerService, Foundation.Kernel" implementationType="Foundation.Kernel.Services.RestartServer.RestartServerService, Foundation.Kernel" lifetime="Singleton" /> <!-- Event Related Services --> <register serviceType="Foundation.Kernel.Services.Events.IEventService, Foundation.Kernel" implementationType="Foundation.Kernel.Services.Events.EventService, Foundation.Kernel" lifetime="Singleton" /> <register serviceType="Foundation.Kernel.Services.Events.IEventManagerService, Foundation.Kernel" implementationType="Foundation.Kernel.Services.Events.EventManagerService, Foundation.Kernel" lifetime="Singleton" /> <register serviceType="Foundation.Kernel.Services.Events.IEventTriggerer, Foundation.Kernel" implementationType="Foundation.Kernel.Services.Events.EventTriggerer, Foundation.Kernel" lifetime="Singleton" /> <!-- Event Handler Services --> <register serviceType="Foundation.Kernel.Events.RestartServer.IRestartLocalServerEventHandler, Foundation.Kernel" implementationType="Foundation.Kernel.Events.RestartServer.RestartLocalServerEventHandler, Foundation.Kernel" lifetime="Singleton" /> <register serviceType="Foundation.Kernel.Events.RestartServer.IRestartRemoteServerEventHandler, Foundation.Kernel" implementationType="Foundation.Kernel.Events.RestartServer.RestartRemoteServerEventHandler, Foundation.Kernel" lifetime="Singleton" /> <!-- Pipeline Processor Services --> <register serviceType="Foundation.Kernel.Pipelines.Initialize.RestartServer.IRestartServerSubscriber, Foundation.Kernel" implementationType="Foundation.Kernel.Pipelines.Initialize.RestartServer.RestartServerSubscriber, Foundation.Kernel" lifetime="Singleton" /> </services> <settings> <!-- This is set here for testing. Ideally, you would have this set for every Sitecore instance you have in your specific custom patch file where identify your server --> <setting name="InstanceName"> <patch:attribute name="value">Sandbox</patch:attribute> </setting> </settings> </sitecore> </configuration>
Considering I had built this to be executed from a PowerShell script hooked into a custom button through the Content Editor Ribbon integration point via Sitecore PowerShell Extensions (SPE), I will be testing this using two simple PowerShell scripts; both will be executed from the Sitecore PowerShell Extensions ISE (why yes, Sitecore MVP Michael West, I had tested this on SPE v6.1.1 😉 ):
Let’s see how we did.
Let’s test this by restarting the current Sitecore instance:
$serviceType = [Foundation.Kernel.Services.RestartServer.IRestartServerService] $service = [Sitecore.DependencyInjection.ServiceLocator]::ServiceProvider.GetService($serviceType) -as $serviceType #use service locator to get the IRestartServerService service $service.RestartCurrentServer() #Let's call the method to restart the current server (CM)
As expected, my Sitecore instance froze up as it was restarting. I then saw this in my logs:
Let’s now restart the remote Sitecore instance:
$serviceType = [Foundation.Kernel.Services.RestartServer.IRestartServerService] $service = [Sitecore.DependencyInjection.ServiceLocator]::ServiceProvider.GetService($serviceType) -as $serviceType #use service locator to get the IRestartServerService service $service.RestartRemoteServer("Sandbox") #Let's call the method to restart the remote server (CD)
Also as expected, my Sitecore instance froze up as it was restarting — remember, I am testing this on a single development instance of Sitecore where I don’t have a separate CM and CD . I then saw this in my logs:
Let me know if you have questions/comments/fears/dreams/hopes/apsirations/whatever by dropping a comment. 😉
Prevent Unbucketable Sitecore Items from Being Moved to Bucket Folders
If you’ve been reading my posts lately, you have probably noticed I’ve been having a ton of fun with Sitecore Item Buckets. I absolutely love this feature in Sitecore.
As a matter of, I love Item Buckets so much, I’m doing a presentation on them just next week at the Greater Cincinnati Sitecore Users Group. If you’re in the neighborhood, stop by — even if it’s only to say “Hello”.
Anyways, back to the post.
I noticed the following grey box on the Items Buckets page on the Sitecore Documentation site:
This got me thinking: why can’t we build something in Sitecore to prevent this from happening in the first place?
In other words, why can’t we just say “sorry, you can’t move an unbucketable Item into a bucket folder”?
So, that’s what I decided to do — build a solution that prevents this from happening. Let’s have a look at what I came up with.
I first created the following interface for classes whose instances will move a Sitecore item to a destination Item:
using Sitecore.Data.Items; namespace Sitecore.Sandbox.Utilities.Items.Movers { public interface IItemMover { bool DisableSecurity { get; set; } bool ShouldBeMoved(Item item, Item destination); void Move(Item item, Item destination); } }
I then defined the following class which implements the interface above:
using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.SecurityModel; namespace Sitecore.Sandbox.Utilities.Items.Movers { public class ItemMover : IItemMover { public bool DisableSecurity { get; set; } public virtual bool ShouldBeMoved(Item item, Item destination) { return item != null && destination != null; } public virtual void Move(Item item, Item destination) { if (!ShouldBeMoved(item, destination)) { return; } if(DisableSecurity) { MoveWithoutSecurity(item, destination); return; } MoveWithoutSecurity(item, destination); } protected virtual void MoveWithSecurity(Item item, Item destination) { Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNull(destination, "destination"); item.MoveTo(destination); } protected virtual void MoveWithoutSecurity(Item item, Item destination) { Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNull(destination, "destination"); using (new SecurityDisabler()) { item.MoveTo(destination); } } } }
Callers of the above code can move an Item from one location to another with/without Sitecore security in place.
The ShouldBeMoved() above is basically a stub that will allow subclasses to define their own rules on whether an Item should be moved, depending on whatever rules must be met.
I then defined the following subclass of the class above which has its own rules on whether an Item should be moved (i.e. move this unbucketable Item out of a bucket folder if makes its way there):
using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Sandbox.Buckets.Util.Methods; using Sitecore.Sandbox.Utilities.Items.Movers; namespace Sitecore.Sandbox.Buckets.Util.Items.Movers { public class UnbucketableItemMover : ItemMover { protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; } public override bool ShouldBeMoved(Item item, Item destination) { return base.ShouldBeMoved(item, destination) && !IsItemBucketable(item) && IsItemInBucket(item) && !IsItemBucketFolder(item) && IsItemBucketFolder(item.Parent) && IsItemBucket(destination); } protected virtual bool IsItemBucketable(Item item) { EnsureItemBucketFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemBucketable(item); } protected virtual bool IsItemInBucket(Item item) { EnsureItemBucketFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemContainedWithinBucket(item); } protected virtual bool IsItemBucketFolder(Item item) { EnsureItemBucketFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemBucketFolder(item); } protected virtual bool IsItemBucket(Item item) { EnsureItemBucketFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemBucket(item); } protected virtual void EnsureItemBucketFeatureMethods() { Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!"); } } }
I’m injecting an instance of an IItemBucketsFeatureMethods class — this interface and its implementation are defined in my previous post; go have a look if you have not read that post so you can be familiar with the IItemBucketsFeatureMethods code — via the Sitecore Configuration Factory which contains common methods I am using in my Item Bucket code solutions (I will be using this in future posts).
The ShouldBeMoved() method basically says that an Item can only be moved when the Item and destination passed aren’t null — this is defined on the base class’ ShouldBeMoved() method; the Item isn’t bucketable; the Item is already in an Item Bucket; the Item isn’t a Bucket Folder; the Item’s parent Item is a Bucket Folder; and the destination is an Item Bucket.
Yes, the above sounds a bit confusing though there is a reason for it — I want to take an unbucketable Item out of a Bucket Folder and move it directly under the Item Bucket instead.
I then created the following class which contains methods that will serve as “item:moved” event handlers:
using System; using System.Collections.Generic; using Sitecore.Data; using Sitecore.Data.Events; using Sitecore.Data.Items; using Sitecore.Events; using Sitecore.Sandbox.Buckets.Util.Methods; using Sitecore.Sandbox.Utilities.Items.Movers; namespace Sitecore.Sandbox.Buckets.Events.Items.Move { public class RemoveFromBucketFolderIfNotBucketableHandler { protected static SynchronizedCollection<ID> ItemsBeingProcessed { get; set; } protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; } protected IItemMover UnbucketableItemMover { get; set; } protected void OnItemMoved(object sender, EventArgs args) { Item item = GetItem(args); RemoveFromBucketFolderIfNotBucketable(item); } static RemoveFromBucketFolderIfNotBucketableHandler() { ItemsBeingProcessed = new SynchronizedCollection<ID>(); } protected virtual Item GetItem(EventArgs args) { if (args == null) { return null; } return Event.ExtractParameter(args, 0) as Item; } protected void OnItemMovedRemote(object sender, EventArgs args) { Item item = GetItemRemote(args); RemoveFromBucketFolderIfNotBucketable(item); } protected virtual Item GetItemRemote(EventArgs args) { ItemMovedRemoteEventArgs remoteArgs = args as ItemMovedRemoteEventArgs; if (remoteArgs == null) { return null; } return remoteArgs.Item; } protected virtual void RemoveFromBucketFolderIfNotBucketable(Item item) { if(item == null) { return; } Item itemBucket = GetItemBucket(item); if (itemBucket == null) { return; } if(!ShouldBeMoved(item, itemBucket)) { return; } AddItemBeingProcessed(item); MoveUnderItemBucket(item, itemBucket); RemoveItemBeingProcessed(item); } protected virtual bool IsItemBeingProcessed(Item item) { if (item == null) { return false; } return ItemsBeingProcessed.Contains(item.ID); } protected virtual void AddItemBeingProcessed(Item item) { if (item == null) { return; } ItemsBeingProcessed.Add(item.ID); } protected virtual void RemoveItemBeingProcessed(Item item) { if (item == null) { return; } ItemsBeingProcessed.Remove(item.ID); } protected virtual Item GetItemBucket(Item item) { if(ItemBucketsFeatureMethods == null || item == null) { return null; } return ItemBucketsFeatureMethods.GetItemBucket(item); } protected virtual bool ShouldBeMoved(Item item, Item itemBucket) { if(UnbucketableItemMover == null) { return false; } return UnbucketableItemMover.ShouldBeMoved(item, itemBucket); } protected virtual void MoveUnderItemBucket(Item item, Item itemBucket) { if (UnbucketableItemMover == null) { return; } UnbucketableItemMover.Move(item, itemBucket); } } }
Both the OnItemMoved() and OnItemMovedRemote() methods extract the moved Item from their specific methods for getting the Item from the EventArgs instance. If that Item is null, the code exits.
Both methods pass their Item instance to the RemoveFromBucketFolderIfNotBucketable() method which ultimately attempts to grab an Item Bucket ancestor of the Item via the GetItemBucket() method. If no Item Bucket instance is returned, the code exits.
If an Item Bucket was found, the RemoveFromBucketFolderIfNotBucketable() method ascertains whether the Item should be moved — it makes a call to the ShouldBeMoved() method which just delegates to the IItemMover instance injected in via the Sitecore Configuration Factory (have a look at the patch configuration file below).
If the Item should not be moved, then the code exits.
If it should be moved, it is then passed to the MoveUnderItemBucket() method which delegates to the Move() method on the IItemMover instance.
You might be asking “Mike, what’s up with the ItemsBeingProcessed SynchronizedCollection of Item IDs?” I’m using this collection to maintain which Items are currently being moved so we don’t have racing conditions in code.
You might be thinking “Great, we’re done!”
We can’t just move an Item from one destination to another, especially when the user selected the first destination. We should let the user know that we will need to move the Item as it is unbucketable. Let’s not be evil.
I created the following class whose Process() method will serve as a custom processor for both the <uiDragItemTo> and <uiMoveItems> pipelines of the Sitecore Client:
using System; using System.Collections.Generic; using System.Linq; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Text; using Sitecore.Web.UI.Sheer; using Sitecore.Sandbox.Buckets.Util.Methods; namespace Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems { public class ConfirmMoveOfUnbucketableItem { protected string ItemIdsParameterName { get; set; } protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; } protected string ConfirmationMessageFormat { get; set; } public void Process(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); IEnumerable<string> itemIds = GetItemIds(args); if (itemIds == null || !itemIds.Any() || itemIds.Count() > 1) { return; } string targetId = GetTargetId(args); if (string.IsNullOrWhiteSpace(targetId)) { return; } Database database = GetDatabase(args); if (database == null) { return; } Item targetItem = GetItem(database, targetId); if (targetItem == null || !IsItemBucketOrIsItemInBucket(targetItem)) { return; } Item item = GetItem(database, itemIds.First()); if (item == null || IsItemBucketable(item)) { return; } Item itemBucket = GetItemBucket(targetItem); if (itemBucket == null) { return; } SetTokenValues(args, item, itemBucket); ConfirmMove(args); } protected virtual IEnumerable<string> GetItemIds(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); string itemIdsParameterName = GetItemIdsParameterName(args); Assert.IsNotNullOrEmpty(itemIdsParameterName, "GetItemIdParameterName() cannot return null or the empty string!"); return new ListString(itemIdsParameterName, '|'); } protected virtual string GetItemIdsParameterName(ClientPipelineArgs args) { Assert.IsNotNullOrEmpty(ItemIdsParameterName, "ItemIdParameterName must be set in configuration!"); Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); return args.Parameters[ItemIdsParameterName]; } protected virtual string GetTargetId(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); return args.Parameters["target"]; } protected virtual Database GetDatabase(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); return Factory.GetDatabase(args.Parameters["database"]); } protected virtual Item GetItem(Database database, string itemId) { Assert.ArgumentNotNull(database, "database"); Assert.ArgumentNotNullOrEmpty(itemId, "itemId"); try { return database.GetItem(itemId); } catch(Exception ex) { Log.Error(ToString(), ex, this); } return null; } protected virtual bool IsItemBucketOrIsItemInBucket(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return IsItemBucket(item) || IsItemInBucket(item); } protected virtual bool IsItemBucket(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemBucket(item); } protected virtual bool IsItemInBucket(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemContainedWithinBucket(item); } protected virtual bool IsItemBucketable(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); return ItemBucketsFeatureMethods.IsItemBucketable(item); } protected virtual Item GetItemBucket(Item item) { EnsureItemBucketsFeatureMethods(); Assert.ArgumentNotNull(item, "item"); if(!ItemBucketsFeatureMethods.IsItemBucket(item)) { return ItemBucketsFeatureMethods.GetItemBucket(item); } return item; } protected virtual void SetTokenValues(ClientPipelineArgs args, Item item, Item itemBucket) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNull(itemBucket, "itemBucket"); args.Parameters["$itemName"] = item.Name; args.Parameters["$itemBucketName"] = itemBucket.Name; args.Parameters["$itemBucketFullPath"] = itemBucket.Paths.FullPath; } protected virtual void ConfirmMove(ClientPipelineArgs args) { Assert.ArgumentNotNull(args, "args"); if(args.IsPostBack) { if (args.Result == "yes") { ClearResult(args); return; } if (args.Result == "no") { args.AbortPipeline(); return; } } else { SheerResponse.Confirm(GetConfirmationMessage(args)); args.WaitForPostBack(); } } protected virtual void ClearResult(ClientPipelineArgs args) { args.Result = string.Empty; args.IsPostBack = false; } protected virtual string GetConfirmationMessage(ClientPipelineArgs args) { Assert.IsNotNullOrEmpty(ConfirmationMessageFormat, "ConfirmationMessageFormat must be set in configuration!"); Assert.ArgumentNotNull(args, "args"); return ReplaceTokens(ConfirmationMessageFormat, args); } protected virtual string ReplaceTokens(string messageFormat, ClientPipelineArgs args) { Assert.ArgumentNotNullOrEmpty(messageFormat, "messageFormat"); Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Parameters, "args.Parameters"); string message = messageFormat; message = message.Replace("$itemName", args.Parameters["$itemName"]); message = message.Replace("$itemBucketName", args.Parameters["$itemBucketName"]); message = message.Replace("$itemBucketFullPath", args.Parameters["$itemBucketFullPath"]); return message; } protected virtual void EnsureItemBucketsFeatureMethods() { Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!"); } } }
The Process() method above gets the Item ID for the Item that is being moved; the Item ID for the destination Item — this is referred to as the “target” in the code above; gets the Database instance of where we are moving this Item; the instances of both the Item and target Item; determines if the Target Item is a Bucket Folder or an Item Bucket; determines if the Item is unbucketable; and then the Item Bucket (this could be the target Item).
If any of of the instances above are null, the code exits.
If the Item is unbucketable but is being moved to a Bucket Folder or Item Bucket, we prompt the user with a confirmation dialog asking him/her whether he/she should like to continue given that the Item will be moved directly under the Item Bucket.
If the user clicks the ‘Ok’ button, the Item is moved. Otherwise, the pipeline is aborted and the Item will not be moved at all.
I then pieced all of the above together via the following patch configuration file:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <buckets> <movers> <items> <unbucketableItemMover type="Sitecore.Sandbox.Buckets.Util.Items.Movers.UnbucketableItemMover, Sitecore.Sandbox" singleInstance="true"> <DisableSecurity>true</DisableSecurity> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> </unbucketableItemMover> </items> </movers> </buckets> <events> <event name="item:moved"> <handler type="Sitecore.Sandbox.Buckets.Events.Items.Move.RemoveFromBucketFolderIfNotBucketableHandler, Sitecore.Sandbox" method="OnItemMoved"> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> <UnbucketableItemMover ref="buckets/movers/items/unbucketableItemMover" /> </handler> </event> <event name="item:moved:remote"> <handler type="Sitecore.Sandbox.Buckets.Events.Items.Move.RemoveFromBucketFolderIfNotBucketableHandler, Sitecore.Sandbox" method="OnItemMovedRemote"> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> <UnbucketableItemMover ref="buckets/movers/items/unbucketableItemMover" /> </handler> </event> </events> <processors> <uiDragItemTo> <processor patch:before="processor[@type='Sitecore.Buckets.Pipelines.UI.ItemDrag, Sitecore.Buckets' and @method='Execute']" type="Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems.ConfirmMoveOfUnbucketableItem, Sitecore.Sandbox" mode="on"> <ItemIdsParameterName>id</ItemIdsParameterName> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> <ConfirmationMessageFormat>You are attempting to move the non-bucketable Item: $itemName to a bucket folder. If you continue, it will be moved directly under the Item Bucket: $itemBucketName ($itemBucketFullPath). Do you wish to continue?</ConfirmationMessageFormat> </processor> </uiDragItemTo> <uiMoveItems> <processor patch:before="processor[@type='Sitecore.Buckets.Pipelines.UI.ItemMove, Sitecore.Buckets' and @method='Execute']" type="Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems.ConfirmMoveOfUnbucketableItem, Sitecore.Sandbox" mode="on"> <ItemIdsParameterName>items</ItemIdsParameterName> <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" /> <ConfirmationMessageFormat>You are attempting to move the non-bucketable Item: $itemName to a bucket folder. If you continue, it will be moved directly under the Item Bucket: $itemBucketName ($itemBucketFullPath). Do you wish to continue?</ConfirmationMessageFormat> </processor> </uiMoveItems> </processors> </sitecore> </configuration>
Let’s see how we did.
Let’s move this unbucketable Item to an Item Bucket:
Yes, I’m sure I’m sure:
I was then prompted with the confirmation dialog as expected:
As you can see, the Item was placed directly under the Item Bucket:
If you have any thoughts on this, please drop a comment.
Rename Sitecore Clones When Renaming Their Source Item
Earlier today I discovered that clones in Sitecore are not renamed when their source Items are renamed — I’m baffled over how I have not noticed this before since I’ve been using Sitecore clones for a while now
I’ve created some clones in my Sitecore instance to illustrate:
I then initiated the process for renaming the source item:
As you can see the clones were not renamed:
One might argue this is expected behavior for clones — only source Item field values are propagated to its clones when there are no data collisions (i.e. a source Item’s field value is pushed to the same field in its clone when that data has not changed directly on the clone — and the Item name should not be included in this process since it does not live in a field.
Sure, I see that point of view but one of the requirements of the project I am currently working on mandates that source Item name changes be pushed to the clones of that source Item.
So what did I do to solve this? I created an item:renamed event handler similar to the following (the one I built for my project is slightly different though the idea is the same):
using System; using System.Collections.Generic; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Events; using Sitecore.Links; using Sitecore.SecurityModel; namespace Sitecore.Sandbox.Data.Clones { public class ItemEventHandler { protected void OnItemRenamed(object sender, EventArgs args) { Item item = GetItem(args); if (item == null) { return; } RenameClones(item); } protected virtual Item GetItem(EventArgs args) { if (args == null) { return null; } return Event.ExtractParameter(args, 0) as Item; } protected virtual void RenameClones(Item item) { Assert.ArgumentNotNull(item, "item"); using (new LinkDisabler()) { using (new SecurityDisabler()) { using (new StatisticDisabler()) { Rename(GetClones(item), item.Name); } } } } protected virtual IEnumerable<Item> GetClones(Item item) { Assert.ArgumentNotNull(item, "item"); IEnumerable<Item> clones = item.GetClones(); if (clones == null) { return new List<Item>(); } return clones; } protected virtual void Rename(IEnumerable<Item> items, string newName) { Assert.ArgumentNotNull(items, "items"); Assert.ArgumentNotNullOrEmpty(newName, "newName"); foreach (Item item in items) { Rename(item, newName); } } protected virtual void Rename(Item item, string newName) { Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNullOrEmpty(newName, "newName"); if (!item.Access.CanRename()) { return; } item.Editing.BeginEdit(); item.Name = newName; item.Editing.EndEdit(); } } }
The handler above retrieves all clones for the Item being renamed, and renames them using the new name of the source Item — I borrowed some logic from the Execute method in Sitecore.Shell.Framework.Pipelines.RenameItem in Sitecore.Kernel.dll (this serves as a processor of the <uiRenameItem> pipeline).
If you would like to learn more about events and their handlers, I encourage you to check out John West‘s post about them, and also take a look at this page on the
Sitecore Developer Network (SDN).
I then registered the above event handler in Sitecore using the following configuration file:
<?xml version="1.0" encoding="utf-8" ?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <events> <event name="item:renamed"> <handler type="Sitecore.Sandbox.Data.Clones.ItemEventHandler, Sitecore.Sandbox" method="OnItemRenamed"/> </event> </events> </sitecore> </configuration>
Let’s take this for a spin.
I went back to my source item, renamed it back to ‘My Cool Item’, and then initiated another rename operation on it:
As you can see all clones were renamed:
If you have any thoughts/concerns on this approach, or ideas on other ways to accomplish this, please share in a comment.
Synchronize IDTable Entries Across Multiple Sitecore Databases Using a Custom publishItem Pipeline Processor
In a previous post I showed a solution that uses the Composite design pattern in an attempt to answer the following question by Sitecore MVP Kyle Heon:
Although I enjoyed building that solution, it isn’t ideal for synchronizing IDTable entries across multiple Sitecore databases — entries are added to all configured IDTables even when Items might not exist in all databases of those IDTables (e.g. the Sitecore Items have not been published to those databases).
I came up with another solution to avoid the aforementioned problem — one that synchronizes IDTable entries using a custom <publishItem> pipeline processor, and the following class contains code for that processor:
using System.Collections.Generic; using System.Linq; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.IDTables; using Sitecore.Diagnostics; using Sitecore.Publishing.Pipelines.PublishItem; namespace Sitecore.Sandbox.Pipelines.Publishing { public class SynchronizeIDTables : PublishItemProcessor { private IEnumerable<string> _IDTablePrefixes; private IEnumerable<string> IDTablePrefixes { get { if (_IDTablePrefixes == null) { _IDTablePrefixes = GetIDTablePrefixes(); } return _IDTablePrefixes; } } private string IDTablePrefixesConfigPath { get; set; } public override void Process(PublishItemContext context) { Assert.ArgumentNotNull(context, "context"); Assert.ArgumentNotNull(context.PublishOptions, "context.PublishOptions"); Assert.ArgumentNotNull(context.PublishOptions.SourceDatabase, "context.PublishOptions.SourceDatabase"); Assert.ArgumentNotNull(context.PublishOptions.TargetDatabase, "context.PublishOptions.TargetDatabase"); IDTableProvider sourceProvider = CreateNewIDTableProvider(context.PublishOptions.SourceDatabase); IDTableProvider targetProvider = CreateNewIDTableProvider(context.PublishOptions.TargetDatabase); RemoveEntries(targetProvider, GetAllEntries(targetProvider, context.ItemId)); AddEntries(targetProvider, GetAllEntries(sourceProvider, context.ItemId)); } protected virtual IDTableProvider CreateNewIDTableProvider(Database database) { Assert.ArgumentNotNull(database, "database"); return Factory.CreateObject(string.Format("IDTable[@id='{0}']", database.Name), true) as IDTableProvider; } protected virtual IEnumerable<IDTableEntry> GetAllEntries(IDTableProvider provider, ID itemId) { Assert.ArgumentNotNull(provider, "provider"); Assert.ArgumentCondition(!ID.IsNullOrEmpty(itemId), "itemId", "itemId cannot be null or empty!"); List<IDTableEntry> entries = new List<IDTableEntry>(); foreach(string prefix in IDTablePrefixes) { IEnumerable<IDTableEntry> entriesForPrefix = provider.GetKeys(prefix, itemId); if (entriesForPrefix.Any()) { entries.AddRange(entriesForPrefix); } } return entries; } private static void RemoveEntries(IDTableProvider provider, IEnumerable<IDTableEntry> entries) { Assert.ArgumentNotNull(provider, "provider"); Assert.ArgumentNotNull(entries, "entries"); foreach (IDTableEntry entry in entries) { provider.Remove(entry.Prefix, entry.Key); } } private static void AddEntries(IDTableProvider provider, IEnumerable<IDTableEntry> entries) { Assert.ArgumentNotNull(provider, "provider"); Assert.ArgumentNotNull(entries, "entries"); foreach (IDTableEntry entry in entries) { provider.Add(entry); } } protected virtual IEnumerable<string> GetIDTablePrefixes() { Assert.ArgumentNotNullOrEmpty(IDTablePrefixesConfigPath, "IDTablePrefixConfigPath"); return Factory.GetStringSet(IDTablePrefixesConfigPath); } } }
The Process method above grabs all IDTable entries for all defined IDTable prefixes — these are pulled from the configuration file that is shown later on in this post — from the source database for the Item being published, and pushes them all to the target database after deleting all preexisting entries from the target database for the Item (the code is doing a complete overwrite for the Item’s IDTable entries in the target database).
I also added the following code to serve as an item:deleted event handler (if you would like to learn more about events and their handlers, check out John West‘s post about them, and also take a look at this page on the
Sitecore Developer Network (SDN)) to remove entries for the Item when it’s being deleted:
using System; using System.Collections.Generic; using System.Linq; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Events; using Sitecore.Data.IDTables; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Events; namespace Sitecore.Sandbox.Data.IDTables { public class ItemEventHandler { private IEnumerable<string> _IDTablePrefixes; private IEnumerable<string> IDTablePrefixes { get { if (_IDTablePrefixes == null) { _IDTablePrefixes = GetIDTablePrefixes(); } return _IDTablePrefixes; } } private string IDTablePrefixesConfigPath { get; set; } protected void OnItemDeleted(object sender, EventArgs args) { if (args == null) { return; } Item item = Event.ExtractParameter(args, 0) as Item; if (item == null) { return; } DeleteItemEntries(item); } private void DeleteItemEntries(Item item) { Assert.ArgumentNotNull(item, "item"); IDTableProvider provider = CreateNewIDTableProvider(item.Database.Name); foreach (IDTableEntry entry in GetAllEntries(provider, item.ID)) { provider.Remove(entry.Prefix, entry.Key); } } protected virtual IEnumerable<IDTableEntry> GetAllEntries(IDTableProvider provider, ID itemId) { Assert.ArgumentNotNull(provider, "provider"); Assert.ArgumentCondition(!ID.IsNullOrEmpty(itemId), "itemId", "itemId cannot be null or empty!"); List<IDTableEntry> entries = new List<IDTableEntry>(); foreach (string prefix in IDTablePrefixes) { IEnumerable<IDTableEntry> entriesForPrefix = provider.GetKeys(prefix, itemId); if (entriesForPrefix.Any()) { entries.AddRange(entriesForPrefix); } } return entries; } private static void RemoveEntries(IDTableProvider provider, IEnumerable<IDTableEntry> entries) { Assert.ArgumentNotNull(provider, "provider"); Assert.ArgumentNotNull(entries, "entries"); foreach (IDTableEntry entry in entries) { provider.Remove(entry.Prefix, entry.Key); } } protected virtual IDTableProvider CreateNewIDTableProvider(string databaseName) { return Factory.CreateObject(string.Format("IDTable[@id='{0}']", databaseName), true) as IDTableProvider; } protected virtual IEnumerable<string> GetIDTablePrefixes() { Assert.ArgumentNotNullOrEmpty(IDTablePrefixesConfigPath, "IDTablePrefixConfigPath"); return Factory.GetStringSet(IDTablePrefixesConfigPath); } } }
The above code retrieves all IDTable entries for the Item being deleted — filtered by the configuration defined IDTable prefixes — from its database’s IDTable, and calls the Remove method on the IDTableProvider instance that is created for the Item’s database for each entry.
I then registered all of the above in Sitecore using the following configuration file:
<?xml version="1.0" encoding="utf-8" ?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <events> <event name="item:deleted"> <handler type="Sitecore.Sandbox.Data.IDTables.ItemEventHandler, Sitecore.Sandbox" method="OnItemDeleted"> <IDTablePrefixesConfigPath>IDTablePrefixes/IDTablePrefix</IDTablePrefixesConfigPath> </handler> </event> </events> <IDTable type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true"> <patch:attribute name="id">master</patch:attribute> <param connectionStringName="master"/> <param desc="cacheSize">500KB</param> </IDTable> <IDTable id="web" type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true"> <param connectionStringName="web"/> <param desc="cacheSize">500KB</param> </IDTable> <IDTablePrefixes> <IDTablePrefix>IDTableTest</IDTablePrefix> </IDTablePrefixes> <pipelines> <publishItem> <processor type="Sitecore.Sandbox.Pipelines.Publishing.SynchronizeIDTables, Sitecore.Sandbox"> <IDTablePrefixesConfigPath>IDTablePrefixes/IDTablePrefix</IDTablePrefixesConfigPath> </processor> </publishItem> </pipelines> </sitecore> </configuration>
For testing, I quickly whipped up a web form to add a couple of IDTable entries using an IDTableProvider for the master database — I am omitting that code for brevity — and ran a query to verify the entries were added into the IDTable in my master database (I also ran another query for the IDTable in my web database to show that it contains no entries):
I published both items, and queried the IDTable in the master and web databases:
As you can see, both entries were inserted into the web database’s IDTable.
I then deleted one of the items from the master database via the Sitecore Content Editor:
It was removed from the IDTable in the master database.
I then published the deleted item’s parent with subitems:
As you can see, it was removed from the IDTable in the web database.
If you have any suggestions for making this code better, or have another solution for synchronizing IDTable entries across multiple Sitecore databases, please share in a comment.