The current edition of this blog post is at:
http://www.sitecore.net/Community/Technical-Blogs/John-West-Sitecore-Blog/Posts/2010/11/Intercepting-Item-Updates-with-Sitecore.aspx
Introduction
After my presentation at dreamcore 2010, someone asked me which of the techniques available in the Sitecore ASP.NET CMS to use for integrating custom logic when data changes. The available techniques for intercepting data updates include at least:
- The item:saved event.
- The item:saving event.
- The saveUI pipeline.
- Item Saved rules.
- Validators.
This post describes some of the criteria that you can use to determine which technique is appropriate for your requirements.
The item:saved Event
You can implement an item:saved event handler to intercept data changes.
Various operations raise the item:saved and item:saving events as described in this section and the next section. For example, updating an item raises the save events, and creating or renaming an item eventually raises save events as well. Therefore, if you write an item:saved or item:saving event handler well, you might be able to avoid writing separate handlers for create, rename, and other events.
Unfortunately, the duplicate command does not raise save events. You can probably safely assume that the user will change the data after duplication, which will eventually raise a save event. Or you can add a processor to the item:duplicated pipeline or any other event or pipeline to raise the save event(s) after those operations.
If you change data from an item:saved event handler, you have to worry about infinite event recursion. A change from within an item:saved event handler would raise a nested item:saved event. You can solve this by immediately exiting if the item being saved is in a static list that you maintain in the handler. If the item is not in the list, add the item to the list at the beginning of the handler, handle the event, and remove the item from the list at the end.
One concern with event handlers is that events fire for save operations in all databases, an you probably only want to handle save events in the Master database. I typically define a Database property in the handler and set that property in handler definition in web.config to the name of the database for which I want to handle events, and then exit the handler if the saved item is in a different database.
Here’s a code template for an item:saved event handler that shows how to change field values:
namespace Sitecore.Sharedsource.Tasks
{
using System;
using System.Collections.Generic;
public class TitleChanger
{
private static readonly SynchronizedCollection<Sitecore.Data.ID> _inProcess =
new SynchronizedCollection<Sitecore.Data.ID>();
public string Database
{
get;
set;
}
public void OnItemSaved(object sender, EventArgs args)
{
Sitecore.Events.SitecoreEventArgs eventArgs = args as Sitecore.Events.SitecoreEventArgs;
Sitecore.Diagnostics.Assert.IsNotNull(eventArgs, "eventArgs");
Sitecore.Data.Items.Item item = eventArgs.Parameters[0] as Sitecore.Data.Items.Item;
Sitecore.Diagnostics.Assert.IsNotNull(item, "item");
if (item.Database != null && String.Compare(item.Database.Name, this.Database) != 0)
{
return;
}
if (_inProcess.Contains(item.ID))
{
return;
}
Sitecore.Data.Fields.TextField title = item.Fields["title"];
if (title == null)
{
return;
}
_inProcess.Add(item.ID);
try
{
using (new Sitecore.Data.Items.EditContext(item))
{
title.Value += new Random().Next();
}
}
catch (Exception ex)
{
throw ex;
}
finally
{
_inProcess.Remove(item.ID);
}
}
}
}
Add the handler to the /configuration/sitecore/events/event element in web.config named item:saved.
<event name="item:saved">
<handler type="Sitecore.Sharedsource.Tasks.TitleChanger, dreamcore" method="OnItemSaved">
<database>master</database>
</handler>
…
Sitecore raises the item:saved event after the save operation, so you can’t access the field values as they were before the item:saved event, and I you can’t determine which fields changed.
Inside the event handler, you can cancel the event:
eventArgs.Result.Cancel = true;
This prevents Sitecore from processing additional event handlers for the event, but does not prevent Sitecore from committing the change, or the UI shrinking effect that indicates to the user that they saved the item. I don’t recommend cancelling a save event.
As you will see from the following example, it looks easier to change values with an item:saving event than an item:saved event. I have successfully used the item:saved event to relocate news items into a hierarchy based on publication date. I hope to have a chance to write a post on that topic.
The item:saving Event
Some of the issues with the item:saved event also apply to the item:saving event. One difference is that Sitecore raises the item:saving event before committing the change, while it raises the item:saved event afterwards. An advantage of using the item:saving event as opposed to the item:saved event is that you can access the values from the item before the save, and the new values, and can prevent the user from saving their changes.
Here’s a code template for an item:saving event that shows how to access the new and old states of the item, and how you can alter the data before the commit.
public void OnItemSaving(object sender, EventArgs args)
{
Sitecore.Events.SitecoreEventArgs eventArgs = args as Sitecore.Events.SitecoreEventArgs;
Sitecore.Diagnostics.Assert.IsNotNull(eventArgs, "eventArgs");
Sitecore.Data.Items.Item updatedItem = eventArgs.Parameters[0] as Sitecore.Data.Items.Item;
Sitecore.Diagnostics.Assert.IsNotNull(updatedItem, "item");
Sitecore.Data.Items.Item existingItem = updatedItem.Database.GetItem(
updatedItem.ID,
updatedItem.Language,
updatedItem.Version);
Sitecore.Diagnostics.Assert.IsNotNull(existingItem, "existingItem");
Sitecore.Data.Fields.TextField title = updatedItem.Fields["title"];
if (title == null)
{
return;
}
title.Value += new Random().Next();
}
Note that you don’t have to establish an editing context, as you are already within an editing transaction. Note also that I could not easily determine which fields had changed.
Cancelling an item:saving event prevents Sitecore from saving the data, but does not stop the shrinking effect that makes it appear to the user that they saved. Note that Sitecore does not include any default handlers for the item:saving event, so use of this event might not be very common.
The saveUI Pipeline
When a user saves an item in the Sitecore user interface, Sitecore invokes the saveUI pipeline. A pipeline is basically a sequence of processors that implement an operation, where each processor is a class that contains a method that implements some portion of the operation.
The saveUI pipeline is most appropriate when you need to interact with the user saving the item. The saveUI pipeline has access to the state of the item before and after the save operation.
One drawback of the saveUI pipeline is that only certain operations invoke the pipeline. For example, if you automatically import data using APIs, Sitecore does not invoke the saveUI pipeline. This may require you to duplicate logic in multiple processes.
Just for demonstration purposes, here’s a prototype saveUI processor that limits the length of field values.
namespace Sitecore.Sharedsource.Pipelines.Save
{
using System;
public class SampleSaveProcessor
{
public int Limit
{
get;
set;
}
public void Process(Sitecore.Pipelines.Save.SaveArgs args)
{
Sitecore.Diagnostics.Assert.ArgumentNotNull(args, "args");
if (args.Items == null)
{
return;
}
if (this.Limit < 1)
{
this.Limit = 100;
}
foreach(Sitecore.Pipelines.Save.SaveArgs.SaveItem saveItem in args.Items)
{
foreach(Sitecore.Pipelines.Save.SaveArgs.SaveField saveField in saveItem.Fields)
{
if (saveField.Value.Length <= this.Limit)
{
continue;
}
Sitecore.Data.Items.Item item = Sitecore.Client.ContentDatabase.GetItem(saveItem.ID);
Sitecore.Data.Fields.Field field = item.Fields[saveField.ID];
string message = String.Format(
"Length {0} of field {1} exceeds limit {2}; prior length was {3}.",
saveField.Value.Length,
Sitecore.StringUtil.GetString(field.Title, field.Name),
this.Limit,
field.Value.Length);
Sitecore.Web.UI.Sheer.SheerResponse.Alert(message, new string[0]);
args.SaveAnimation = false;
args.AbortPipeline();
return;
}
}
}
}
}
And here’s the corresponding update to web.config,
<saveUI>
<processor mode="on" type="Sitecore.Pipelines.Save.BeforeSaveEvent, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.ParseXml, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.CheckItemLock, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.CheckRevision, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.Validators, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.ValidateFields, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.HasWritePermission, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.NewVersion, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.TightenRelativeImageLinks, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.ConvertToXHtml, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.CheckLock, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.Lock, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.CheckTemplateFieldChange, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.ConvertLayoutField, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.CheckLinks, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Sharedsource.Pipelines.Save.SampleSaveProcessor, assembly"/>
<processor mode="on" type="Sitecore.Pipelines.Save.Save, Sitecore.Kernel"/>
<processor mode="off" type="Sitecore.Pipelines.Save.RenderingHack, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.Unlock, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.WorkflowSaveCommand, Sitecore.Kernel"/>
<processor mode="on" type="Sitecore.Pipelines.Save.PostAction, Sitecore.Kernel"/>
</saveUI>
Notice how the processor must precede the Save processor to abort the pipeline and prevent the save visual effect in the user interface.
Item Saved Rules
Item saved rules let you process item updates using the rules engine. You can read about the rules engine in the Rules Engine Cookbook on the Sitecore Developer Network. One of the default event handlers for the item:saved event invokes item saved rules – item saved rules run any time the item:saved event fires.
Probably the primary advantage of using the rules engine as opposed to an event handler is that you can control the logic and parameters through a browser-based user interface instead of using the web.config file. Another advantages of using the rules engine is separation of concerns: instead of writing a save handler that runs for each save event, which must check whether it should do anything with every item, you write that check into the condition part of the rule. Also, Sitecore runs event handlers for save events in all databases, where item saved rules only run for items updated in the database that contains the rule definition item.
Some potential drawbacks of using item saved rules are similar to those using an item:saved event handler. For example, you cannot access the prior state of the item from an item saved rule, and you cannot stop the save operation from an item saved rule.
You can see an example of using item saved rules in my post about how to Use the Sitecore 6.1 Rules Engine to Control Item Names.
Validators
Validators are not specifically for handling item updates, but you can use them for this purpose. You can use a field validator and set the error level to fatal or critical to prevent the user from saving their changes. Field validators can access the old and new values for the field. Field validators can also provide the user with validation actions, which they can invoke through the user interface to automatically correct data error conditions. You can get more information about validators from my post about how to Validate a Sitecore Checklist, Multilist, Treelist, or TreelistEx Field, which contains links to pages, posts, and threads containing much more information about validation, including the Client Configuration Cookbook.
Conclusion
Depending on your requirements, you can use one or more of these techniques to intercept data changes. Investigate the existing validators, item saved rules, event handler and pipeline definitions to see when Sitecore itself uses each of these approaches. Think about whether you need to block the save operation and the shrinking effect, access the state of the item prior to the save (item:saving, saveUI, or validator), or interact with the user (saveUI or validator). You can implement a saveUI pipeline processor and an event handler for a single event, for example to ensure you trap user and automated updates.
Think about at least the following criteria to determine which approach or approaches to apply:
- If you need to operate on the save process or only the saved item, consider the item:saved event or item saved rules.
- If you need to access the previous values of any field or property from before the change operation, as opposed to only the current values, consider the saveUI pipeline, the item:saving event, or a field validator,
- If you need to provide a user interface to define criteria for handling data changes, consider the rules engine.
- If you need to be able to block the save operation, consider the saveUI pipeline, the item:saving event, or field validators. If you need to prevent the visual effect in the user interface that indicates that Sitecore has saved the selected item, consider the saveUI pipeline or field validators.
- If you need to trap data changes made through APIs, as opposed to only changes made through UIs, do something more than a saveUI pipeline processor.
- Do you need to trap only save, or additional operations such as create, duplicate, and rename?
- Which approach or approaches provide the best user experience?
You can use multiple techniques. For instance, you can implement logic in the saveUI pipeline and the item:saved event to achieve the same objective. You can combine multiple techniques. For example, it should be possible to add a processor to the saveUI pipeline defined in web.config to apply rules using the rules engine.
Update: You can’t be too confident in intercepting data changes. For example, what if a rule changes or you fix a defect in your action code and you need to apply that rule to all existing items. You could use something like a scheduled task to iterate all the items and invoke a process, such as to invoke item saved rules. You can read about scheduled tasks in my previous post, All About Sitecore Scheduling: Agents and Tasks.