Adding custom messages to the publish dialog in Sitecore

Sitecore has pretty sophisticated event handling and pipeline hooks functionality built in. What this means is that almost any area of the user experience can be changed to provide a more customized user experience, tailored to the business needs of the users of the Sitecore implementation. This can be done very easily by writing your own code/class, and then wiring it up using the configuration directives. Event handlers are the easiest to implement – what you need to do is make your own class, define your own method in that class, then just add your include config file with the method/class for that event:


<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <events>
      <event name="publish:end">
        <handler patch:after="handler[@type='Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel']" type="CustomClassName, CustomAssemblyName" method="CustomMethodName" />
      </event>
    </events>
  </sitecore>
</configuration>

In your method, you get what item is being published and what language is being published from the publisher options. Note that the event triggers once for each language and target combination.


Sitecore.Publishing.Publisher p = (Sitecore.Publishing.Publisher)((Sitecore.Events.SitecoreEventArgs)args).Parameters[0];
Item itemBeingPublished = p.Options.RootItem;
Language languageBeingPublished = p.Options.Language;
Database targetDatabase = p.Options.TargetDatabase.Name

There a myriad of events available (http://sdn.sitecore.net/Articles/API/Using%20Events.aspx)- in this post, I’m concentrating on customizing the publish:end event to add messages to the dialog at the end of the publish job. The publish dialog shows informational content about what was published in the job:

The publish dialog at the end of the publish job

The publish dialog at the end of the publish job

Depending on how complex your publish:end code is, you may want to add some informational text here, to show that whatever you tried to do was successful, or not, or just output some information about what it did. Adding to this message stack is done like so:


var publishJobs = Sitecore.Jobs.JobManager
                    .GetJobs().Where(x => x.Category.Equals("publish"))
                    .ToList();

                foreach (Job j in publishJobs)
                {
                    if (j.Handle.IsLocal)
                    {
                        j.Status.Messages.Add(System.Environment.NewLine + "Your custom message goes here." + System.Environment.NewLine);
                    }

                }

You can add this at any point in your code for the event handler, and it will show in the final screen. If you are throwing exceptions in your code, however, it will show in the screen before this, where it will show the message of the exception. I usually found it cleaner to show it on the final screen, and I also don’t want to interrupt the actual publishing of the items. This is for a publish:end event, so it happens after the publish event is complete, so the actual items are already published. Depending on your scenario, you may want to kill the publishing and interrupt it, in which case an exception may work better. Once added, your message will show like this:

The publish end dialog final screen with the messages inserted as part of the publish:end event code

The publish end dialog final screen with the messages inserted as part of the publish:end event code

An edited version of this post also appears on The Runtime Report.

Sitecore Clones explained in more detail: Part 2

In Sitecore Clones explained in more detail: Part 1 – we went over a few items things about clones that could be useful in deciding how and where to use them, and their basic function. In this post, I’ll go over a specific code-related solution we had to apply to get all our business cases implemented.

Auto accept of the notifications when parent item is changed (or a new item is added under a parent item that has been cloned)

One of things mentioned in the previous post was about how clones replicate themselves, and keep themselves in sync. It is by way of notifications that are created when the clone is changed or new items added under the parent item. A content editor then has to accept the change or reject it. If accepted, the resulting change from the parent is then copied over/implemented into the clone. This is great when editors want manual control over every piece of content. However, we had a business case scenario where the clones and the parent item were not controlled by the same department. The department that manages the tree section that has the clones are allowed to add in new items, but are not allowed to change the cloned items. So whenever a parent item is changed, those changes need to propagate automatically, meaning the notifications need to be auto accepted. To solve this, we added an event handler for item:saved event, which is triggered whenever an item is changed and saved. We need to create a class, such as ForceCloneAccept.cs, and add a include config to wire it up:


<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <events>
      <event name="item:saved">
        <handler patch:after="handler[@type='Sitecore.Data.Fields.ItemEventHandler, Sitecore.Kernel']" type="HenrySchein.CMS.Business.EventHandlers.ForceCloneAccept, HenrySchein.CMS.Business" method="OnItemSaved" />
      </event>
    </events>
  </sitecore>
</configuration>

The code for ForceCloneAccept.cs is as below:


using System;
using System.Linq;
using Sitecore.Data.Events;
using Sitecore.Data.Items;
using Sitecore.Events;
using Sitecore;
using Sitecore.SecurityModel;
using Sitecore.Data.Clones;
using Sitecore.Diagnostics;
using System.Collections.Generic;
using Sitecore.Links;
using Sitecore.Jobs;
using Sitecore.Data.Proxies;
using Sitecore.Workflows;
using Sitecore.Data;
using Sitecore.Configuration;
using System.Configuration;

namespace EventHandlers
{
    public class ForceCloneAccept
    {
        Database master = Factory.GetDatabase("master");

        public void OnItemSaved(object sender, EventArgs args)
        {
            using (new Sitecore.SecurityModel.SecurityDisabler())
            {
                var item = Event.ExtractParameter(args, 0) as Item;

                if (item != null && !item.HasClones)
                {
                    if (item.Parent == null || !item.Parent.HasClones) return;

                    var jobOptions = new JobOptions("AcceptChanges", string.Empty, Context.GetSiteName(), this, "AcceptChanges", new object[] { item.Parent });
                    var job = new Job(jobOptions);
                    jobOptions.InitialDelay = new TimeSpan(0, 0, 0, 1, 0);
                    JobManager.Start(job);
                }
                else
                {
                    var jobOptions = new JobOptions("AcceptChanges", string.Empty, Context.GetSiteName(), this, "AcceptChanges", new object[] { item });
                    var job = new Job(jobOptions);
                    jobOptions.InitialDelay = new TimeSpan(0, 0, 0, 1, 0);
                    JobManager.Start(job);
                }
            }
        }

        public void AcceptChanges(Item args)
        {
            using (new SecurityDisabler())
            {
                var item = args;
                var clones = item.GetClones(true);
                foreach (var clone in clones)
                {
                    var notifications = clone.Database.NotificationProvider.GetNotifications(clone);
                    foreach (var notification in notifications)
                    {
                        //if (notification.GetType() != typeof(FieldChangedNotification))
                        //{
                            clone.Database.NotificationProvider.RemoveNotification(notification.ID);
                            notification.Accept(clone);
                        //}

                    }

                    clone.Editing.BeginEdit();
                    try
                    {
                        clone.Fields["__Workflow"].Value = args.Fields["__Workflow"].Value;
                        clone.Fields["__Workflow state"].Value = args.Fields["__Workflow state"].Value;
                    }
                    finally
                    {
                        clone.Editing.EndEdit();

                    }

                }
            }
        }

        public static List GetItemClones(Item item, bool processChildren)
        {
            var realItem = ProxyManager.GetRealItem(item, false);

            var list = Globals.LinkDatabase.GetReferrers(realItem).Where(link => !(link.SourceFieldID != FieldIDs.Source)).Select(link => link.GetSourceItem()).Where(sourceItem => sourceItem != null).ToList();

            if (processChildren)
            {
                foreach (Item item4 in realItem.Children)
                {
                    list.AddRange(GetItemClones(item4, true));
                }
            }
            return list;
        }

    }
}

If you know how event handlers work, you already know that the way it is wired up, the method that will get triggered for this event is OnItemSaved; in this method we will find the context item (the item that got saved) and check for its clones, and get all the notifications each of the clone and accept them. Few things to note about this code:

    1. Notice that instead of applying the changes right in the OnItemSaved method, we are kicking off a job with a 1 second delay. Why? Because the notifications do not get added to the clones until after the item:saved event has been completed. So if you were to check the notification for that item at the time of this code running, there would be no notifications. There are other methods that you could also hook into, such as item:added, or item:created, but one of them has been deprecated, and the other only hits the first time. item:saved is something that gets called all the time, so this is more reliable. It will also get called when a new language version is added, or when new numbered version is added.

    2. There are several different types of notification that get created, based on the changes to the parent item. The notification types can be for a field change, for a child created, for an item that moved, or a version created. In AcceptChanges method, once you get all the notifications for the cloned item, you check and auto-accept only certain notifications.

    3. Clones by default do not inherit workflow. So, workflow fields are not copied over at all, even on the notification changes. The easiest (and somewhat crude) way to do this is to manually copy the fields over, so when a publish job runs, it will publish the cloned items also.

    4. Because we are only accepting notifications, and not really concerned with the changes are, we don’t have to write code to figure out the changes are. This simplifies things, and lets Sitecore handle applying the actual changes.

Similar to how we are force accepting the notifications, you can definitely write code to do anything your specific business scenario requires.

An edited version of this post also appears on The Runtime Report.

Sitecore Clones explained in more detail: Part 1

Sitecore clones were introduced first in version 6.3, as a way to centralize content items. It is a very useful way to place your items throughout the content tree, and then have the content controlled from one central location in the content tree. This is, at least the basic usage. There are other granular way to fine-tune this usage, where each of the cloned items have more control over themselves.

I’m not going to go very far into how to use clones, what they are and how they function – it is pretty well documented in the Sitecore documentation, and various Sitecore expert blogs, such as John West’s Blog. What I will detail here are some of the complications that arise when using clones, and how I tackled them.

1. Basic Usage of clones

As stated before, the basic usage of clones is to ‘clone‘ or create a copy of an item in the content tree to another different location within the same content tree. One parent item can have multiple cloned items. What this allows you to do is control the content once, in the parent item, and all the child cloned items will also get the changes (there is more to this, in the following points).

To simply create a clone, first choose the item you want to clone (News Item for Cloning), and then go to configure tab in the ribbon, and click ‘clone

Add a clone of an item by going to Configure->Clone

Add a clone of an item by going to Configure->Clone

When you click ‘clone‘, and a popup will show with the content tree – you then have to choose where the cloned item will be created. Once you hit ‘clone‘ button in the popup, the cloned item will be created. (I chose /home/news):

Cloned item created (shown in grey)

Cloned item created (shown in grey)

Notice that the cloned item is shown in grey. This is one way to tell whether an item is a clone.

Another way to tell if an item is a clone is to see the ‘Quick Info‘ tab – A cloned item will have a value for ‘Created From‘, which points to the original parent item.

From within code, you can check the values of item.IsClone and/or item.SourceUri

Item 'Quick Info' section

Item ‘Quick Info’ section

2. Changes to the cloned item, the parent item, and new items created under the parent.

Once a clone is created, you can still make changes to the clone. Since now there is a link between the parent item and the cloned item, it is somewhat assumed that if I change any fields in the parent item, the cloned item must change also. This is partially correct, but there are a few things that also happens:

    a. If the original value of the field and the value of the field in the cloned item is the same, it applies the changes immediately. When you click on the cloned item, you can see the change immediately.

    You can always click on a cloned item and see which values are the original value (from the parent) and which values have been changed in the cloned item itself – it will state [original value] to designate all fields that are exactly the same as the parent item.

    Cloned item field values

    Cloned item field values

    b. If the value in the field has been changed in the cloned item, and it does not match the parent item, and the value is changed in the parent item, it is not automatically applied (as of version 6.6). In this scenario when anything is changed in the parent item, and you want to see that change in the cloned item, there is an approval process. Sitecore creates a notification, which the user must accept in order to apply the change to the cloned item. For example, changing the title in the News Item For Cloning item creates a notification in the cloned item, which shows up once you click on the cloned item:

    Cloned item field change notification

    Cloned item field change notification

    At this point, you can review the original item to see the changes, accept the changes (which will overwrite the values that has been changed in the cloned item) or reject the change, which effectively keep the values that the cloned item has. If the changes are rejected, the field will not say [original value] anymore for all the fields that do not match the value in the field of the parent item. Subsequent changes to the same fields in the parent item will recreate these notifications.

    This also applies to new sub-items created under the parent item – in this case a different notification appears:

    Cloned item new item created nofication

    Cloned item new item created nofication

    As same with the other notification, the user has the ability to accept or reject the change. In this case if the change is rejected, the sub-item never appears under the cloned item. The entire tree must be re-cloned in order for the new sub-item to get cloned.

For an effective complete control of all cloned items, we have to write code to handle item:saved events for any items that have been cloned, and automatically accept the changes to all cloned items. I’m hoping in the future versions of Sitecore will give us a way to configure (when cloning) to designate whether a change must be forced or not, so there can much more granular content over each piece of item.

For examples of how to write event handlers for this, see here: http://adeneys.wordpress.com/2010/11/02/sharing-content-with-item-clones/

Note: I’ve also written code to do this: if you need some code samples, I will explain this in more detail in Part 2 very shortly. If you need the samples right away, leave a comment and I will get back to you..

3. Managing cloned items from workflow and publishing perspective

Cloned items are nothing but regular Sitecore items, with a link between the cloned item and the parent item. Once published, though, cloned item becomes real items, and the relationship between the items no longer exist. This is not an issue, because on the web database, this relationship is not needed. But remember that all changes to cloned items must be individually published, and just publishing the parent item does not publish the cloned items.

In respect to this, the cloned items can have their own workflow, even if field value changes are forced (via an event handler). So in fact, a parent item can be changed, approved and published, while the cloned items are still in a draft mode. To solve this scenario, cloned items can be made to follow the same approval process by copying the workflow properties during the event handler process which auto-accepts the notifications (more in Part 2).

4. Appearance of cloned items

As of version 6.6, the cloned items are always in grey. There is no way to configure a different style for this, other than writing custom code for it. You can add your processor to the uiCloneItems pipeline processor, and change style of the item as such:

clonedItem.Fields[Sitecore.FieldIDs.Style].Value = "color:#666666; font-style: italic";

More details about how to write the event handlers for auto-accept and their usages coming in Part 2.

An edited version of this post also appears on The Runtime Report.