Variation Strategy #2 to remove label overwrites – cancel updates!
In this first Variation Strategy post, the solution was not the most elegant but the only out of the box one.
The main problem I’m facing with customers is that they simply want variations on ItemCreated only. They do not care about updates done in the original language as to their impact to the other languages. Worst, the mechanism today not only overwrites a target page from the source (which is in another language), but you cannot simply go back to the original target page in the right language – you have to reapprove it!
Unfortunately, as far as I have been able to discuss with the product group, this behaviour isn’t changing at this moment. By looking at the events fired up for the current Variation mechanism, I can see why as there are tons of use cases.
So the first post is really what I recommend customer going for as it stays OOB only and doesn’t affect the product. This 2nd post will detail a 2nd solution that plays with the product but doesn’t affect its underlying core.
Let’s quickly go through how variations works :
- You define a source variation
- You can choose to have manual creation propagation or automatic for either site or site+pages
- What this means is that once a page is propagated, it is linked and any published updates to the source will kick in the variation propagation again.
- Every time a page is approved in a source variation, it will log the event in a list
- Once every minute, there is a timer job that reads the list and propagate any required sites and pages
- If a target page exists but isn’t linked, an error will be raised
- If a target page is linked but checked-out, a force check-in will occur and it will then update the page with the content from the source
- If a target page doesn’t exist, it will create it and add an entry in the Variation Relationship list.
As for the process for when a page get updated:
- You have a source page, published as version 1.0, with page content “EN”
- You have a target and linked page, published as version 1.0 as well, but with page content “FR”
- You update the source to “EN – 2” and publish it as version 2.0
- After the Timer Job passes, the linked target page will have page content “EN – 2” as version 1.1.
- Visitors will still see “FR” from version 1.0
- Content managers will however see “EN – 2”
- There is no rollback, you have to check-out the page, retrieve the previous published version, and re-publish (or re-approve) it!.
This last portion is what customers do not like in general. They’d like to have the ability to have the relationship list but not the items to be updated in target labels every time. While it may make sense in some content update cases, there’s often little changes in a page that doesn’t need to be replicated to all labels.
Solutions to block updates?
So if you ask yourself what you can do, you will end up with 2 possible solutions:
- Can we intercept the update and cancel it if the update comes from the source?
- Can we manage a relationship list on our own based on ItemCreated only?
Solution #1 wasn’t a definite until I looked at it – and I’ll come back to it as this is the reason for this post; Solution #2 is appealing but isn’t that easy. Maintaining a relationship list means:
- Creating your own Language Picker control (granted, you should anyway as the out of the box one will give you the link even if the page hasn’t been published yet)
- Create your own Labels screen which will allow pre-creation of label and post-creation of label
- Create your own Event Receiver for ItemAdded that will kick in only for your source variation. This will create the same page on a target label but it will not have content since we do not have it on ItemAdded – we’d have to hook on ItemUpdating/ed to do this but this is the point, we do not want to.
- Create your own Event Receiver for WebDeleting to manage the relationship list
- The Relationship List will contain IDs for sites and pages so that your language picker control can provide the link to the right page.
- You may want to have an Update Variation link to force a push down of content, a variation log view, etc.
And I might be forgetting some things – but worst, if the product changes, you aren’t using the real variations. It does offer some advantages and I’ll probably try this solution in the future – but this would be a last resort.
Can we intercept the update and cancel if the updates comes from the source?
Let’s go back to Solution #1. The answer is yes but it wasn’t that easy to catch all (hopefully all…) use case. First of all, there was 2 way :
- You could check who is doing the update. If it is the system, it *could* be the variation mechanism – however, it could be the approval workflow as well so this isn’t good
- You can add a Publishing Field (like HTML) to all pages and manage its content. It will contain a different value at the source than the targets.
So the solution breaks down into these blocks:
- A Site Collection Feature that, on activation, adds the Publishing Html Site Column and adds it to the Page content type. It will also loop through all webs and activate the Web Feature.
- On deactivation, it will remove the column from all Pages list in all webs, remove the column in the site content type, and delete the site column. It will also loop through all webs to deactivate the web feature.
- A Web Feature that, on activation, add 3 event receivers for ItemAdding, ItemUpdating, and ItemUpdated.
- On deactivation, it will remove the events on the Pages list
- An Event Receiver class that will intercept the 3 events
- On ItemAdding, we will set the value of the field to either “source” or “target”
- On ItemUpdating, we will validate if the AfterProperties are set to “source” while the ListItem contains “target”
- If so, we cancel the update
- On ItemUpdated, we will force to set the value if it didn’t kick in previously.
- Note: I did this one because of some rare case where you activate/deactivate/reactivate features – you will have pages that may have been created without going through ItemAdded – the value isn’t right.
- A Feature that staples the other Features to your Site Definitions so that when you create your sub-sites, it will activate the Web feature automatically.
There are also various use cases to keep in mind:
- Ability to stop or start this feature at any time (i.e.: if the product changes in the future, you will want to stop this package without any impact) -- this means that a page may not contain the ‘VariationStatus’ value, or a target may contain ‘source’ if you had the the feature running at one point, stopped it, and updated the source.
- Support for updating the target page (obvious, but it’s the same event)
- Support for when you have or don’t have versioning
- Support for Content Deployment
- Ability to work on an existing site
- Support over the current variations without error messages
What will not work:
- Any updates from Variations after the first one will not go through (well, that’s the point). This means that if an editor uses the “update variations”, it will say it worked but no updates will have gone through
- I think I have a case where, after updating the source page, if you click “modify”, it will say you need a refresh first. I’ll have to look again into this one – it may be fixed now as I used SPListItem.SystemUpdate(false) at one point in ItemAdding.
The reason for this, and it’s also why this solution works, is that there are 2 ItemUpdating events. The first one contains the updated fields – you want to cancel it; the 2nd contains what I assume is necessary for the variation to work (the relationship, the logs, etc.) – you let that one go through.
The bulk of the solution lies in having a Publishing Field – which is the only thing brought at every updates from the Variation propagation job – that contains a different value at the source than target.
Note: All tests were made with an English Site Collection – I’ll have to test when it’s not the case as the Variation Labels list may have a different name.
Code - Constants
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5:
6: namespace MB.SharePoint.ApplicationLogic
7: {
8: public static class Consts
9: {
10: public static class Guids
11: {
12: public const string contentTypePage = "0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF39";
13:
14: public const string VariationOnCreateOnlySite = "{3389D85A-4122-4fb3-B46E-C6A3B404F2C1}";
15: public const string VariationOnCreateOnlyWeb = "{CFF1173C-688D-4e43-9878-F660803625DC}";
16: }
17:
18:
19: public static class Fields
20: {
21: public const string GroupName = "MB";
22: public const string variationOnCreateOnly = "MBVariationStatus";
23: }
24:
25: public static class FieldTypeNames
26: {
27: public const string RichHtml = "HTML";
28: }
29:
30: public static class Values
31: {
32: public const string variationOnCreateOnlySourceValue = "source";
33: public const string variationOnCreateOnlyDestinationValue = "target";
34:
35:
36: }
37:
38: }
39: }
Code – Utility methods
1: using System;
2: using Microsoft.SharePoint;
3: using Microsoft.SharePoint.Publishing;
4:
5: namespace MB.SharePoint.ApplicationLogic
6: {
7: public class Utils
8: {
9: private static object _lock = new object();
10: private static string sourceVariationUrl = "";
11:
12:
13:
14: #region Columns and Content Types
15: public static void AddSiteColumn(SPSite site, string fieldTypeName, string fieldName, bool hidden, bool required, string description, string group, string defaultValue)
16: {
17: SPWeb web = site.RootWeb;
18:
19: if (!web.Fields.ContainsField(fieldName))
20: {
21: //add the field if it doesn't exists
22: SPField newField = web.Fields.CreateNewField(fieldTypeName, fieldName) as SPField;
23: newField.Hidden = hidden;
24: newField.Required = required;
25: newField.Description = description;
26: newField.Group = group;
27: if (newField != null)
28: newField.DefaultValue = defaultValue;
29: web.Fields.Add(newField);
30: web.Update();
31: }
32: }
33:
34:
35: public static void AddSiteColumnToContentType(SPSite site, SPContentTypeId contentTypeID, string fieldTypeName, string fieldName, bool hidden, bool required, string description, string group, string defaultValue)
36: {
37: SPWeb web = site.RootWeb;
38: SPContentType contentType = web.ContentTypes[contentTypeID];
39:
40: if (contentType != null)
41: {
42: AddSiteColumn(site, fieldTypeName, fieldName, hidden, required, description, group, defaultValue);
43:
44: if (contentType.FieldLinks[fieldName] == null)
45: {
46: //link the field to the content type
47: contentType.FieldLinks.Add(new SPFieldLink(web.Fields[fieldName]));
48: contentType.Update(true, false);
49: }
50: }
51:
52: }
53:
54: #endregion
55:
56:
57:
58: #region Publishing
59: public static bool isSourceVariation(string url, SPWeb web)
60: {
61: string sourceUrl = sourceVariationUrl;
62: if (sourceUrl == null || sourceUrl.Length == 0)
63: {
64: lock (_lock)
65: {
66: sourceUrl = sourceVariationUrl;
67: if (sourceUrl == null || sourceUrl.Length == 0)
68: {
69: //Get Source variation from list -- we cannot use API since we do not have a context
70: using (SPWeb rootWeb = web.Site.RootWeb)
71: {
72: try
73: {
74: //get labels
75: SPList labelsList = rootWeb.GetList("/variation labels");
76: SPQuery query = new SPQuery();
77:
78: query.Query = "<Where><Eq><FieldRef Name='Is_x0020_Source'/><Value Type='Boolean'>1</Value></Eq></Where>";
79: SPListItemCollection labels = labelsList.GetItems(query);
80: foreach (SPListItem item in labels)
81: {
82: sourceUrl = rootWeb.ServerRelativeUrl.ToUpper() + ((string)item["Title"]).ToUpper();
83: sourceVariationUrl = sourceUrl;
84: break;
85: }
86: }
87: catch (Exception)
88: {
89: //problem reading variation list, return false by default
90: return false;
91: }
92: }
93: }
94: }
95: }
96:
97: return url.ToUpper().StartsWith(sourceUrl);
98: }
99:
100:
101: public static void DeleteFieldFromAllPagesLists(SPWeb web, string fieldName)
102: {
103: if (PublishingWeb.IsPublishingWeb(web))
104: {
105: PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web);
106: SPList pages = pubWeb.PagesList;
107: if (pages.Fields.ContainsField(Consts.Fields.variationOnCreateOnly))
108: {
109: pages.Fields.Delete(Consts.Fields.variationOnCreateOnly);
110: pages.Update();
111: }
112:
113: pubWeb.Close();
114: }
115:
116:
117: if (web.Webs != null)
118: {
119: foreach (SPWeb subWeb in web.Webs)
120: {
121: DeleteFieldFromAllPagesLists(subWeb, fieldName);
122: subWeb.Dispose();
123: }
124: }
125: }
126:
127: #endregion
128:
129:
130:
131: #region EventReceivers
132: public static void AddEventReceiver(SPList list, string className, string assemblyName, SPEventReceiverType type, int sequenceNumber)
133: {
134:
135: bool isNewEvent = true;
136: foreach (SPEventReceiverDefinition eventReceiver in list.EventReceivers)
137: {
138: if (eventReceiver.Class.ToUpper().Equals(className) && eventReceiver.Type == type)
139: {
140: isNewEvent = false;
141: break;
142: }
143: }
144:
145:
146: //Add Event Receiver
147: if (isNewEvent)
148: {
149: SPEventReceiverDefinition eventReceiver = list.EventReceivers.Add();
150: eventReceiver.Type = type;
151: eventReceiver.Assembly = assemblyName;
152: eventReceiver.Class = className;
153: eventReceiver.SequenceNumber = sequenceNumber;
154: eventReceiver.Update();
155: }
156: }
157:
158: public static void DeleteEventReceiver(SPList list, string className, SPEventReceiverType type)
159: {
160: foreach (SPEventReceiverDefinition eventReceiver in list.EventReceivers)
161: {
162: if (eventReceiver.Class.ToUpper().Equals(className.ToUpper()) && eventReceiver.Type == type)
163: {
164: eventReceiver.Delete();
165: break;
166: }
167: }
168: }
169:
170: public static bool IsItemUpdatingFromCheckin(SPItemEventProperties properties)
171: {
172: return (properties.AfterProperties["vti_sourcecontrolcheckedoutby"] == null && properties.BeforeProperties["vti_sourcecontrolcheckedoutby"] != null);
173: }
174: #endregion
175:
176:
177:
178: #region Features
179: public static void ActivateFeatureRecursively(SPWeb web, Guid featureId, bool force, bool checkForPublishing)
180: {
181: if ((checkForPublishing && PublishingWeb.IsPublishingWeb(web)) || !checkForPublishing)
182: web.Features.Add(featureId, force);
183:
184: foreach (SPWeb subWeb in web.Webs)
185: {
186: ActivateFeatureRecursively(subWeb, featureId, force, checkForPublishing);
187: subWeb.Dispose();
188: }
189: }
190:
191: public static void DeactivateFeatureRecursively(SPWeb web, Guid featureId, bool force, bool checkForPublishing)
192: {
193: if (((checkForPublishing && PublishingWeb.IsPublishingWeb(web)) || !checkForPublishing) && web.Features[featureId] != null)
194: web.Features.Remove(featureId, force);
195:
196: foreach (SPWeb subWeb in web.Webs)
197: {
198: DeactivateFeatureRecursively(subWeb, featureId, force, checkForPublishing);
199: subWeb.Dispose();
200: }
201: }
202: #endregion
203:
204:
205: #region General
206:
207: public static bool IsInEditMode()
208: {
209: return SPContext.Current.FormContext.FormMode == Microsoft.SharePoint.WebControls.SPControlMode.Edit || SPContext.Current.FormContext.FormMode == Microsoft.SharePoint.WebControls.SPControlMode.New;
210: }
211:
212: #endregion
213: }
214: }
You can see that I have a method to validate if it’s from the source. This is due to the fact that the APIs for variations only work if you have an SPContext – which you don’t in a Timer Job.
Code – Site Feature Receiver
1: using System;
2: using Microsoft.SharePoint;
3: using Microsoft.SharePoint.Publishing;
4: using MB.SharePoint.ApplicationLogic;
5:
6: namespace MB.SharePoint.FeatureReceivers
7: {
8: public class VariationOnCreateOnlySite : Microsoft.SharePoint.SPFeatureReceiver
9: {
10: public override void FeatureActivated(SPFeatureReceiverProperties properties)
11: {
12: SPSite site = (SPSite)properties.Feature.Parent;
13:
14: //add column to Page in Site Columns and apply change everywhere
15: ApplicationLogic.Utils.AddSiteColumnToContentType(site, new SPContentTypeId(Consts.Guids.contentTypePage), Consts.FieldTypeNames.RichHtml, Consts.Fields.variationOnCreateOnly, false, false, "Allows blocking of variation updates", Consts.Fields.GroupName, "awef");
16:
17: //Activate the Web Feature on all publishing site
18: ApplicationLogic.Utils.ActivateFeatureRecursively(site.RootWeb, new Guid(Consts.Guids.VariationOnCreateOnlyWeb), false, true);
19: }
20:
21: public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
22: {
23:
24: SPWeb web = ((SPSite)properties.Feature.Parent).RootWeb;
25: SPContentTypeCollection contentTypes = web.ContentTypes;
26:
27: //Remove column
28: if (web.Fields.ContainsField(Consts.Fields.variationOnCreateOnly))
29: {
30: //the field is hidden, we cannot delete it until we make it reappear
31: SPField field = web.Fields[Consts.Fields.variationOnCreateOnly];
32: field.Hidden = false;
33: field.Update(true);
34: }
35:
36: SPContentType contentType = web.ContentTypes[Consts.Guids.contentTypePage];
37:
38: if (contentType != null)
39: {
40: if (contentType.Fields.ContainsField(Consts.Fields.variationOnCreateOnly))
41: {
42: //remove linked field from Content Type
43: if (contentType.FieldLinks[Consts.Fields.variationOnCreateOnly] != null)
44: {
45: contentType.FieldLinks.Delete(Consts.Fields.variationOnCreateOnly);
46: contentType.Update(true);
47: }
48:
49: //remove from libraries
50: ApplicationLogic.Utils.DeleteFieldFromAllPagesLists(web, Consts.Fields.variationOnCreateOnly);
51:
52: //remove field from Web
53: if (web.Fields.ContainsField(Consts.Fields.variationOnCreateOnly))
54: web.Fields.Delete(Consts.Fields.variationOnCreateOnly);
55: }
56: }
57:
58:
59: //Deactivate VariationOnCreateOnlyWeb everywhere
60: ApplicationLogic.Utils.DeactivateFeatureRecursively(web, new Guid(Consts.Guids.VariationOnCreateOnlyWeb), false, true);
61:
62: }
63:
64: public override void FeatureInstalled(SPFeatureReceiverProperties properties)
65: {
66: //nothing
67: }
68:
69: public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
70: {
71: //nothing
72: }
73: }
74: }
Code – Web Feature Receiver
1: using System;
2: using Microsoft.SharePoint;
3: using Microsoft.SharePoint.Publishing;
4:
5: namespace MB.SharePoint.FeatureReceivers
6: {
7: class VariationOnCreateOnlyWeb : Microsoft.SharePoint.SPFeatureReceiver
8: {
9: public override void FeatureActivated(SPFeatureReceiverProperties properties)
10: {
11: //add events
12: SPWeb web = ((SPWeb)properties.Feature.Parent);
13: SPList pages = web.GetList(web.ServerRelativeUrl.TrimEnd('/') + "/pages");
14: ApplicationLogic.Utils.AddEventReceiver(pages, "MB.SharePoint.EventReceivers.VariationsOnCreateOnlyEventReceiver", "MB.SharePoint.EventReceivers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0ca89b26b6d2ebc5", SPEventReceiverType.ItemAdding, 10000);
15: ApplicationLogic.Utils.AddEventReceiver(pages, "MB.SharePoint.EventReceivers.VariationsOnCreateOnlyEventReceiver", "MB.SharePoint.EventReceivers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0ca89b26b6d2ebc5", SPEventReceiverType.ItemUpdating, 10001);
16: ApplicationLogic.Utils.AddEventReceiver(pages, "MB.SharePoint.EventReceivers.VariationsOnCreateOnlyEventReceiver", "MB.SharePoint.EventReceivers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0ca89b26b6d2ebc5", SPEventReceiverType.ItemUpdated, 10002);
17: }
18:
19: public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
20: {
21: //remove events
22: SPWeb web = ((SPWeb)properties.Feature.Parent);
23: SPList pages = web.GetList(web.ServerRelativeUrl.TrimEnd('/') + "/pages");
24: ApplicationLogic.Utils.DeleteEventReceiver(pages, "MB.SharePoint.EventReceivers.VariationsOnCreateOnlyEventReceiver", SPEventReceiverType.ItemAdding);
25: ApplicationLogic.Utils.DeleteEventReceiver(pages, "MB.SharePoint.EventReceivers.VariationsOnCreateOnlyEventReceiver", SPEventReceiverType.ItemUpdating);
26: ApplicationLogic.Utils.DeleteEventReceiver(pages, "MB.SharePoint.EventReceivers.VariationsOnCreateOnlyEventReceiver", SPEventReceiverType.ItemUpdated);
27: }
28:
29: public override void FeatureInstalled(SPFeatureReceiverProperties properties)
30: {
31: //nothing
32: }
33:
34: public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
35: {
36: //nothing
37: }
38: }
39: }
Code – Event Receivers
1: using System;
2: using Microsoft.SharePoint;
3: using MB.SharePoint.ApplicationLogic;
4:
5: namespace MB.SharePoint.EventReceivers
6: {
7: public class VariationsOnCreateOnlyEventReceiver : SPItemEventReceiver
8: {
9: //Will set the source and target variation value
10: public override void ItemAdding(SPItemEventProperties properties)
11: {
12: //if not source variation, update variationStatus to source
13: using (SPWeb web = properties.OpenWeb())
14: {
15: properties.AfterProperties[Consts.Fields.variationOnCreateOnly] = Utils.isSourceVariation(properties.RelativeWebUrl, web) ? ApplicationLogic.Consts.Values.variationOnCreateOnlySourceValue : ApplicationLogic.Consts.Values.variationOnCreateOnlyDestinationValue;
16: base.ItemAdded(properties);
17: }
18: }
19:
20: //Cancel variation depending on where the update originated
21: public override void ItemUpdating(SPItemEventProperties properties)
22: {
23: using (SPWeb contextWeb = properties.OpenWeb())
24: {
25: string fieldInternalName = contextWeb.Lists[properties.ListId].Fields[Consts.Fields.variationOnCreateOnly].InternalName;
26: bool isSourceVariation = Utils.isSourceVariation(properties.RelativeWebUrl, contextWeb);
27:
28: string updatedValue = isSourceVariation ? Consts.Values.variationOnCreateOnlySourceValue : Consts.Values.variationOnCreateOnlyDestinationValue;
29: string afterValue = (string)properties.AfterProperties[fieldInternalName];
30:
31: bool isUpdatingFromSource = false;
32: if (afterValue != null && properties.ListItem != null && properties.ListItem[fieldInternalName] != null)
33: isUpdatingFromSource = isSourceVariation ? true : (afterValue.Equals(Consts.Values.variationOnCreateOnlySourceValue) && !properties.ListItem[fieldInternalName].Equals(Consts.Values.variationOnCreateOnlySourceValue));
34: bool cancel = !isSourceVariation && isUpdatingFromSource;
35:
36:
37: if (cancel)
38: {
39: properties.Status = SPEventReceiverStatus.CancelNoError;
40: properties.Cancel = true;
41: }
42: else
43: {
44: DisableEventFiring();
45: properties.AfterProperties[fieldInternalName] = updatedValue;
46: base.ItemUpdating(properties);
47: EnableEventFiring();
48: }
49: }
50: }
51:
52: //in some scenario, pages already created before activating the site collection, this ensures a value in the
53: public override void ItemUpdated(SPItemEventProperties properties)
54: {
55: using (SPWeb contextWeb = properties.OpenWeb())
56: {
57: string fieldInternalName = contextWeb.Lists[properties.ListId].Fields[Consts.Fields.variationOnCreateOnly].InternalName;
58: bool isSourceVariation = Utils.isSourceVariation(properties.RelativeWebUrl, contextWeb);
59: string updatedValue = isSourceVariation ? Consts.Values.variationOnCreateOnlySourceValue : Consts.Values.variationOnCreateOnlyDestinationValue;
60:
61: if (!properties.ListItem[fieldInternalName].Equals(updatedValue))
62: {
63: DisableEventFiring();
64: properties.ListItem[fieldInternalName] = updatedValue;
65: properties.ListItem.SystemUpdate(true);
66: EnableEventFiring();
67: }
68: }
69: }
70: }
71: }
Note: I think I can be good without the ItemUpdated now but I’ll have to re-run a full test run to validate.
Here is the complete solution to this. The WSP will install all of this and will staple on the out of the box Publishing Site with and without Workflows (+ press release). The Features and Site Column aren’t hidden – this was to help validate and debug – and you can make them hidden if necessary.
Note: The key is password-protected, just skip the password and create your own if you want to recompile. You’ll want to change the feature manifests for your key and the VariationOnCreateOnlyWeb.cs file.
Disclaimer : Note, this code is provided *AS IS* and no support is available from Microsoft. While I will do what I can – when I can – to help you find a problem in the code, this code should be tested thoroughly in your environment before sending in production.
As a last note, I believe this solution to be the next best solution if you do not want an empty source variation (for creation only) but do not want to erase target pages at each source update. The reason for this is that it’s not breaking up the OOB feature, we can stop this at any time without impact, and there are no errors in the logs.
Comments
Anonymous
February 25, 2011
Maxime, Very content-intensive posts! Thanks a lot.Anonymous
July 21, 2011
Thanks a lot for interesting article. But eventrecievers are not fired for target site when content is being updated by variation jobs. Are there chances that variation jobs might have disabled event firing? Do we need to enable those explicitly? Kindly help.Anonymous
July 21, 2011
Is that for SP2010 or SP2007? With 2010, this essentially becomes obsolete as you can do it with PowerShell. As for 2007, events are still triggered when the target site (page rather) is updated -- as long as you deployed the feature and activated it on all sites that you need it.