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 :
As for the process for when a page get updated:
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.
So if you ask yourself what you can do, you will end up with 2 possible solutions:
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:
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.
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 :
So the solution breaks down into these blocks:
There are also various use cases to keep in mind:
What will not work:
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.
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: }
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:
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;
19: if (!web.Fields.ContainsField(fieldName))
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:
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.
4: using MB.SharePoint.ApplicationLogic;
6: namespace MB.SharePoint.FeatureReceivers
8: public class VariationOnCreateOnlySite : Microsoft.SharePoint.SPFeatureReceiver
10: public override void FeatureActivated(SPFeatureReceiverProperties properties)
12: SPSite site = (SPSite)properties.Feature.Parent;
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: }
36: SPContentType contentType = web.ContentTypes[Consts.Guids.contentTypePage];
38: if (contentType != null)
39: {
40: if (contentType.Fields.ContainsField(Consts.Fields.variationOnCreateOnly))
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);
52: //remove field from Web
53: if (web.Fields.ContainsField(Consts.Fields.variationOnCreateOnly))
54: web.Fields.Delete(Consts.Fields.variationOnCreateOnly);
55: }
56: }
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)
66: //nothing
67: }
68:
69: public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
70: {
71: //nothing
72: }
73: }
74: }
5: namespace MB.SharePoint.FeatureReceivers
7: class VariationOnCreateOnlyWeb : Microsoft.SharePoint.SPFeatureReceiver
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: }
19: public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
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
34: public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
35: {
36: //nothing
37: }
3: using MB.SharePoint.ApplicationLogic;
5: namespace MB.SharePoint.EventReceivers
7: public class VariationsOnCreateOnlyEventReceiver : SPItemEventReceiver
9: //Will set the source and target variation value
10: public override void ItemAdding(SPItemEventProperties properties)
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);
18: }
19:
20: //Cancel variation depending on where the update originated
21: public override void ItemUpdating(SPItemEventProperties properties)
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;
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: }
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();
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.