This is something that I put together a while ago but I’m only just now getting to the point where I can document it. I was looking for a solution to a common problem of propagating changes to content types deployed via a Feature and I came across a post by Søren Nielsen. Søren created a custom stsadm command which handles pushing down the content type changes. I didn’t want to re-invent the wheel but I needed the ability to call the code in different ways and I wanted to try my hand at using the SPContentTypeUsages class so I decided to use what he created and just refactor it to meet my goals. Søren does a great job at explaining the problem and what the code is doing so I won’t reiterate it here. My modified version of the code can be seen below:
1using System;
2using System.Collections.Generic;
3using System.Collections.Specialized;
4using System.Diagnostics;
5using System.Text;
6using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
7using Lapointe.SharePoint.STSADM.Commands.SPValidators;
8using Microsoft.SharePoint;
9
10namespace Lapointe.SharePoint.STSADM.Commands.ContentTypes
11{
12 /// <summary>
13 /// This code was derived from Søren Nielsen's code that he provides on his blog:
14 /// http://soerennielsen.wordpress.com/2007/09/11/propagate-site-content-types-to-list-content-types/
15 /// </summary>
16 public class PropagateContentType : SPOperation
17 {
18 /// <summary>
19 /// Initializes a new instance of the <see cref="PropagateContentType"/> class.
20 /// </summary>
21 public PropagateContentType()
22 {
23 SPParamCollection parameters = new SPParamCollection();
24 parameters.Add(new SPParam("url", "url", true, null, new SPNonEmptyValidator()));
25 parameters.Add(new SPParam("contenttype", "ct", true, null, new SPNonEmptyValidator()));
26 parameters.Add(new SPParam("verbose", "v"));
27 parameters.Add(new SPParam("updatefields", "uf"));
28 parameters.Add(new SPParam("removefields", "rf"));
29
30 StringBuilder sb = new StringBuilder();
31 sb.Append("\r\n\r\nPropagates a site scoped content type to list scoped instances of that content type.\r\n\r\nParameters:");
32 sb.Append("\r\n\t-url <site collection url>");
33 sb.Append("\r\n\t-contenttype <content type name>");
34 sb.Append("\r\n\t[-verbose]");
35 sb.Append("\r\n\t[-updatefields]");
36 sb.Append("\r\n\t[-removefields]");
37
38 Init(parameters, sb.ToString());
39 }
40
41 /// <summary>
42 /// Gets the help message.
43 /// </summary>
44 /// <param name="command">The command.</param>
45 /// <returns></returns>
46 public override string GetHelpMessage(string command)
47 {
48 return HelpMessage;
49 }
50
51 /// <summary>
52 /// Runs the specified command.
53 /// </summary>
54 /// <param name="command">The command.</param>
55 /// <param name="keyValues">The key values.</param>
56 /// <param name="output">The output.</param>
57 /// <returns></returns>
58 public override int Execute(string command, StringDictionary keyValues, out string output)
59 {
60 output = string.Empty;
61
62 using (SPSite site = new SPSite(Params["url"].Value.TrimEnd('/')))
63 {
64 Process(site, Params["contenttype"].Value, Params["verbose"].UserTypedIn, Params["updatefields"].UserTypedIn,
65 Params["removefields"].UserTypedIn);
66 }
67 return OUTPUT_SUCCESS;
68 }
69
70 /// <summary>
71 /// Processes the content type.
72 /// </summary>
73 /// <param name="site">The site.</param>
74 /// <param name="contentTypeName">Name of the content type.</param>
75 /// <param name="verbose">if set to <c>true</c> [verbose].</param>
76 /// <param name="updateFields">if set to <c>true</c> [update fields].</param>
77 /// <param name="removeFields">if set to <c>true</c> [remove fields].</param>
78 public void Process(SPSite site, string contentTypeName, bool verbose, bool updateFields, bool removeFields)
79 {
80 Verbose = verbose;
81 try
82 {
83 Log("Pushing content type changes to lists for '" + contentTypeName + "'");
84 // get the site collection specified
85 using (SPWeb rootWeb = site.RootWeb)
86 {
87 //Get the source site content type
88 SPContentType sourceCT = rootWeb.AvailableContentTypes[contentTypeName];
89 if (sourceCT == null)
90 {
91 throw new ArgumentException("Unable to find Content Type named \"" + contentTypeName + "\"");
92 }
93
94 IList<SPContentTypeUsage> ctUsageList = SPContentTypeUsage.GetUsages(sourceCT);
95 foreach (SPContentTypeUsage ctu in ctUsageList)
96 {
97 if (!ctu.IsUrlToList)
98 continue;
99
100 using (SPWeb web = site.OpenWeb(ctu.Url))
101 {
102
103 SPList list = web.GetList(ctu.Url);
104 SPContentType listCT = list.ContentTypes[ctu.Id];
105 ProcessContentType(list, sourceCT, listCT, updateFields, removeFields);
106 }
107 }
108 }
109 return;
110 }
111 catch (Exception ex)
112 {
113 Log("Unhandled error occured: " + ex.Message, EventLogEntryType.Error);
114 throw;
115 }
116 finally
117 {
118 Log("Finished pushing content type changes to lists for '" + contentTypeName + "'");
119 }
120 }
121
122 /// <summary>
123 /// Processes the content type.
124 /// </summary>
125 /// <param name="list">The list.</param>
126 /// <param name="sourceCT">The source CT.</param>
127 /// <param name="listCT">The list CT.</param>
128 /// <param name="updateFields">if set to <c>true</c> [update fields].</param>
129 /// <param name="removeFields">if set to <c>true</c> [remove fields].</param>
130 private static void ProcessContentType(SPList list, SPContentType sourceCT, SPContentType listCT, bool updateFields, bool removeFields)
131 {
132 if (listCT == null)
133 return;
134
135 Log("Processing content type on list:" + list);
136
137 if (updateFields)
138 {
139 UpdateListFields(list, listCT, sourceCT);
140 }
141
142 //Find/add the fields to add
143 foreach (SPFieldLink sourceFieldLink in sourceCT.FieldLinks)
144 {
145 if (!FieldExist(sourceCT, sourceFieldLink))
146 {
147 Log(
148 "Failed to add field "
149 + sourceFieldLink.DisplayName + " on list "
150 + list.ParentWebUrl + "/" + list.Title
151 + " field does not exist (in .Fields[]) on "
152 + "source content type", EventLogEntryType.Warning);
153 }
154 else
155 {
156 if (!FieldExist(listCT, sourceFieldLink))
157 {
158 //Perform double update, just to be safe
159 // (but slow)
160 Log("Adding field \""
161 + sourceFieldLink.DisplayName
162 + "\" to contenttype on "
163 + list.ParentWebUrl + "/" + list.Title,
164 EventLogEntryType.Information);
165 if (listCT.FieldLinks[sourceFieldLink.Id] != null)
166 {
167 listCT.FieldLinks.Delete(sourceFieldLink.Id);
168 listCT.Update();
169 }
170 listCT.FieldLinks.Add(new SPFieldLink(sourceCT.Fields[sourceFieldLink.Id]));
171 listCT.Update();
172 }
173 }
174 }
175
176 if (removeFields)
177 {
178 //Find the fields to delete
179 //WARNING: this part of the code has not been
180 // adequately tested (though what could go wrong? … 🙂
181
182 //Copy collection to avoid modifying enumeration as we go through it
183 List<SPFieldLink> listFieldLinks = new List<SPFieldLink>();
184 foreach (SPFieldLink listFieldLink in listCT.FieldLinks)
185 {
186 listFieldLinks.Add(listFieldLink);
187 }
188
189 foreach (SPFieldLink listFieldLink in listFieldLinks)
190 {
191 if (!FieldExist(sourceCT, listFieldLink))
192 {
193 Log("Removing field \""
194 + listFieldLink.DisplayName
195 + "\" from contenttype on :"
196 + list.ParentWebUrl + "/"
197 + list.Title, EventLogEntryType.Information);
198 listCT.FieldLinks.Delete(listFieldLink.Id);
199 listCT.Update();
200 }
201 }
202 }
203 }
204
205 /// <summary>
206 /// Updates the fields of the list content type (listCT) with the
207 /// fields found on the source content type (courceCT).
208 /// </summary>
209 /// <param name="list">The list.</param>
210 /// <param name="listCT">The list CT.</param>
211 /// <param name="sourceCT">The source CT.</param>
212 private static void UpdateListFields(SPList list, SPContentType listCT, SPContentType sourceCT)
213 {
214 Log("Starting to update fields ", EventLogEntryType.Information);
215 foreach (SPFieldLink sourceFieldLink in sourceCT.FieldLinks)
216 {
217 //has the field changed? If not, continue.
218 if (listCT.FieldLinks[sourceFieldLink.Id] != null
219 && listCT.FieldLinks[sourceFieldLink.Id].SchemaXml
220 == sourceFieldLink.SchemaXml)
221 {
222 Log("Doing nothing to field \"" + sourceFieldLink.Name
223 + "\" from contenttype on :" + list.ParentWebUrl + "/"
224 + list.Title, EventLogEntryType.Information);
225 continue;
226 }
227 if (!FieldExist(sourceCT, sourceFieldLink))
228 {
229 Log(
230 "Doing nothing to field: " + sourceFieldLink.DisplayName
231 + " on list " + list.ParentWebUrl
232 + "/" + list.Title + " field does not exist (in .Fields[])"
233 + " on source content type", EventLogEntryType.Information);
234 continue;
235
236 }
237
238 if (listCT.FieldLinks[sourceFieldLink.Id] != null)
239 {
240
241 Log("Deleting field \"" + sourceFieldLink.Name
242 + "\" from contenttype on :" + list.ParentWebUrl + "/"
243 + list.Title, EventLogEntryType.Information);
244
245 listCT.FieldLinks.Delete(sourceFieldLink.Id);
246 listCT.Update();
247 }
248
249 Log("Adding field \"" + sourceFieldLink.Name
250 + "\" from contenttype on :" + list.ParentWebUrl
251 + "/" + list.Title, EventLogEntryType.Information);
252
253 listCT.FieldLinks.Add(new SPFieldLink(sourceCT.Fields[sourceFieldLink.Id]));
254 //Set displayname, not set by previous operation
255 listCT.FieldLinks[sourceFieldLink.Id].DisplayName = sourceCT.FieldLinks[sourceFieldLink.Id].DisplayName;
256 listCT.Update();
257 Log("Done updating fields ");
258 }
259 }
260
261 /// <summary>
262 /// Fields the exist.
263 /// </summary>
264 /// <param name="contentType">Type of the content.</param>
265 /// <param name="fieldLink">The field link.</param>
266 /// <returns></returns>
267 private static bool FieldExist(SPContentType contentType, SPFieldLink fieldLink)
268 {
269 try
270 {
271 //will throw exception on missing fields
272 return contentType.Fields[fieldLink.Id] != null;
273 }
274 catch (Exception)
275 {
276 return false;
277 }
278 }
279 }
280}
By refactoring the code slightly I’m now able to use the code via the stsadm command, which I called gl-propagatecontenttype
, or I can call the Process method via my Feature Receiver by just adding a reference to the assembly – this way I can push changes to content types down to the lists they are bound to when my Feature is re-activated. Here’s the syntax of the command:
C:\>stsadm -help gl-propagatecontenttype
stsadm -o gl-propagatecontenttype
Propagates a site scoped content type to list scoped instances of that content type.
Parameters:
-url <site collection url>
-contenttype <content type name>
[-verbose]
[-updatefields]
[-removefields]
I suggest you avoid the use of the -removefields
parameter if possible – I left it there because I thought I might need it but it’s usually not a good thing to do something so destructive in batch like that (just make sure you at least test the change before going to production with it).
Again – props to Søren – I just retooled his code some.