X

Register now for unlimited access to Sitecore resources.


Already have an account? Log in now

*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
X

Request a demo

It’s easy to get started. Sign up for a personalized demo.

*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
Sitecore Blog: @sitecorejohn blog

Repost: Intercepting Item Updates with Sitecore

By John West, November 12, 2010 | Rating:  | Comments (12)

This blog post describes techniques that you can use to invoke custom logic when data changes in the Sitecore ASP.NET CMS.

This is a repost of http://sitecorejohn.spaces.live.com/blog/cns!960125F1D4A59952!822.entry.

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:

  1. The item:saved event. For more information about events, see my blog post All About Events in the Sitecore ASP.NET CMS.
  2. The item:saving event.
  3. The saveUI pipeline. For more information about pipelines, see my blog post All About Pipelines in the Sitecore ASP.NET CMS.
  4. Item Saved rules. For more information about the rules engine, see my blog post All About the Sitecore ASP.NET CMS Rules Engine
  5. Validators. For more information about validation, see my blog post All About Validation with the Sitecore ASP.NET CMS.

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 (SDN). 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 the Use the Sitecore 6.1 Rules Engine to Control Item Names post on my old blog.

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.

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.

Tags: API, Architecture, Infrastructure

Comments

  • "If you change data from an item:saved event handler, you have to worry about infinite event recursion... You can solve this by immediately exiting if the item being saved is in a static list that you maintain in the handler."

    This may not be thread safe though, right? Dealing with a similar situation in a item:versionAdded handler. Maybe safer to store it in Sitecore.Context.Items, which should be thread safe?

    e.g.
    Sitecore.Context.Items[myKey] = processedItem;

    - Nick Wesselman
    November 10, 2011 at 1:45 PM

  • I am attempting to use the item:saving event to add an audit trail to certain content items. This seemed to work great at first, where I could compare the event arg's Item fields against the existing item fields and build out a log of what changed and the values that were changed. That is, until I realized that I was only getting values for field values that had been edited, not added. If the field contained a blank value before the edit, the field does not even show up in the FieldCollection of the editing item. Unfortunate.

    I am able to execute this behavior using the saveUI pipeline, but I was really hoping to have this work for ALL save events, not just ones took place via the UI.

    - Ashley Ashley
    March 12, 2012 at 10:27 AM

  • Is it safe to save a second item in the "saving" or "saved" events for an item? For example, if two items are set up with links to each other, and the link to Item B gets removed from Item A can we have an event that removes the opposite link (link to Item A from Item B)? Is there any sort of transaction wrapped around a "save" event? Or could the original save succeed and the "event-driven" save fail and we'd be stuck in a bad state with regards to these links?

    - Charles Boyung
    September 24, 2012 at 1:29 PM

  • @Charles: I think it is safe to use item:saving and item:saved handlers for this purpose (and I have done that, and know of others that have), but to my knowledge, there is still no concept of transactional updates in Sitecore. If the update to B fails, I believe it should throw an exception, and you could use that to manually rollback the update to A (either avoid the EndEdit, or rethrow from within the EditContext for A, or just don't trap it in the EditContext for A?). I think it would be unsafe if an update to A caused an update to B and an update to B caused an update to A, as you could end up with an infinite loop. In such a case, I think you could pass false as the second parameter when creating the EditContext for B to prevent events from firing.

    Anyway this is one of the reasons you might want to use an OnItemSaved rule - because you can put the logic in a rules engine action, and if the OnItemSaved rule fails for some reason, run a rule against all items that checks for the condition and invokes that action again. Whereas triggering a save event for all affected items could take more work.

    http://www.sitecore.net/Community/Technical-Blogs/John-West-Sitecore-Blog/Posts/2012/05/Apply-Rules-to-All-Items-in-the-Sitecore-ASPNET-CMS.aspx

    - John West
    September 24, 2012 at 2:25 PM

  • Thanks, John. All excellent information and this definitely helped us figure out what the best way to handle this situation is. I think we're going to take a look at both events and the OnItemSaved rule option and see how they both work out.

    - Charles Boyung
    September 25, 2012 at 7:48 AM

  • John, I tried what you suggested to use the "silent" parameter on the EndEdit call to prevent events from firing when updating the secondary item, but that causes a problem where the Content Editor UI now doesn't show that updated content for the secondary item until IIS has been restarted. The database has the correct value, but if I go out to the item to view it, it still shows the relationship as not being there. I restart IIS, load the item, and the correct data now shows up. I'm guessing there's some internal event that is now being prevented from firing due to this.

    - Charles Boyung
    September 25, 2012 at 3:01 PM

  • @Charles: Sorry about that; sounds like a caching issue. Maybe try Sitecore.Data.Items.Item.Reload() afterwards, but I am not sure if that does what you need here.Or does it work correctly if you pass true (or nothing) for that second parameter, or would/does that break something else?

    - John West
    September 25, 2012 at 3:07 PM

  • Hi Charles,

    Fancy seeing you here. We handle this case in a different way, by using Sitecore.Context.Items to store state and determine if our event handler is already firing.

    string contextKey = this.GetType().FullName;
    var item = Event.ExtractParameter(args, 0) as Item;
    IList<Item> processingItems = Sitecore.Context.Items[contextKey] as IList<Item>;
    if (processingItems != null && processingItems.Any(pItem => pItem.ID == item.ID))
    {
    return;
    }

    And then if we make it past and are processing items:

    Sitecore.Context.Items[contextKey] = items.ToList();

    Hope this helps or at least stirs some ideas.

    - Nick Wesselman
    September 25, 2012 at 3:13 PM

  • Nick - Thanks, that worked perfectly for what we need to do.

    - Charles Boyung
    September 26, 2012 at 7:41 AM

  • Hi can someone tell me when the item:saved:remote event is invoked? Thanks

    - Mike James
    February 19, 2014 at 3:09 AM

  • Mike - This would happen in a staged environment (separate CM and CD servers, with event queue enabled) during a publish. The event will be fired from the CM and handled on the CD server.

    - Nick Wesselman
    February 19, 2014 at 8:37 AM

  • I believe Sitecore should fire that event after any item:saved event on all instances other than the one experiencing the actual save activity in load-balanced environments. The instance accessed on which the CMS user performed the save should see item:saved while all other servers should see item:saved:remote. You generally don't want multiple instances (those other than the server accessed by the CMS user) to handle an item:saved event, but those other instances may need to be aware of the event to clear caches or otherwise maintain state.

    - John West
    February 19, 2014 at 8:45 AM

Commenting is temporary disabled due to maintenance work.

*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.