zondag 6 februari 2011

Provisioning publishing content (like images) in XML

In most of our SharePoint projects we notice that we need to provision at least some publishing content. What we normally do is creating a separate WSP that’s called something like ‘DefaultContent’ or ‘ExampleContent’.

In this WSP we tend to use some images and publish them to the publishingImages –library and some elements.xml files to define the pages itself. In this XML file we describe the basic content for the page, like publishing-html and text fields.

Like:
1 <file name="Page1.aspx" 
2 path="default.aspx" 
3 type="GhostableInLibrary" 
4 url="page1.aspx">
5 <property name="Title" value="My page"/>
6 <property name="PublishingContent" 
7 value="&lt;h1&gt;My page content&lt;/h1&gt;"/>
8 </file>

Looks familiar, right?

Now we would like to set the image we just deployed to the publishingimages library.
What we would like to do is add another property like this:

1 <property name="PublishingPageImage" 
2 value="~SiteCollection/PublishingImages/image1.jpg"/>

Unfortunately we can’t do this, so we used to set the image fields afterwards in the FeatureActivated event. In this event receiver we will check in the images (if we use a sandboxed solution, since this will not happen automatically) and create some ImageFieldValue-objects to set the page fields.

It’s obvious why we would like to change this, having one entity separated is never desirable from a maintenance point of view.

Added to this the amount of work involved in settings something as simple as an image. You just cannot explain to your customer that setting an image, which would take them 10 seconds, takes us 30 minutes. (and not forget when to checkout , check in and optionally publish the file).

The idea

Define all pages completely in XML, next read this xml in the event receiver and create the corresponding FieldValue’s dependent of what’s defined in the XML.

The code for doing this cannot (always) be put in the same WSP. Reading XML is not, or not easy, to do in a sandboxed solutions. And since we, offcourse, try to make as much solutions sandboxed as possible, we will create a separate assembly for this event receiver.

Since we will use this code in many solutions, and having multiple instances of one piece of code is never a good idea, putting this event receiver in a separate assembly is what we would like to do.

So, we will make a separate assembly for this event receiver that will function like some kind of full trust proxy. This will allow us the provision our publishing pages in a sandboxed solution. Because of this it’s preferable to combine this event receiver with an earlier creation of mine, the PublishingCheckinHelper (sandboxed solutions do not automatically check in or publish content).

So what to do?

To realize this we need to:
  • Define in XML a string notation of the FieldValue
  • Check-out the page (if not sandboxed)
  • Create publishing content values from the string notations and set these fields.
  • Check in the page

Simple enough, right?


Code

So let’s start with the XML notation.

Most publishing fields are easy to define as an URL. If it’s a multivalue field we could easily separate these URLs with a semicolon (;).

It Looks simple, but if we would deploy this the page will end up looking like this:


Not completely what we had in mind, is it?

Now to the event receiver that does make this possible.

There isn’t that much to it. Just one class that inherits from the SPFeatureReceiver and one other class to do the actual work.



We could also add the PublsihingCheckinHelper I made earlier. For clearity I wont't (See upcoming blog for this).

The Event receiver itself is very straightforward:
1 [Guid("041da204-99a3-4ae4-8d30-ab9790e49d91")]
2 public class PublishingContentEventReceiver : SPFeatureReceiver
3 {
4 public override void FeatureActivated(SPFeatureReceiverProperties properties)
5 {
6 SPWeb currentWeb;
7 SPSite currentSite = properties.Feature.Parent as SPSite;
8 
9 if (currentSite != null)
10 currentWeb = currentSite.RootWeb;
11 else if (properties.Feature.Parent is SPWeb) currentWeb =
12 properties.Feature.Parent as SPWeb;
13 else throw new SPException(@"This feature-receiver can
14 only handle web- or site- scoped features");
15 
16 PublishingFieldsTransformer transformer = 
17 new PublishingFieldsTransformer(currentWeb);
18 transformer.TransformAllFromDefinition(
19 properties.Definition.GetXmlDefinition(CultureInfo.InvariantCulture));
20 
21 
22 // Optionally use the PublishingFieldsCheckinHelper
23
24 // PublishingFieldsCheckinHelper checkinHelper = 
new PublishingFieldsCheckinHelper(currentWeb);
25 // checkinHelper.CheckinAllFilesFromDefinition(
26 //properties.Definition.GetXmlDefinition(CultureInfo.InvariantCulture),true);
27 //
28 // //if publishingfeature is enabled we could also publish the files
29 // checkinHelper.PublishAllFilesFromDefinition(
30 // properties.Definition.GetXmlDefinition(CultureInfo.InvariantCulture),true);
31 
32 currentWeb.Update();
33 }
34 }


We just delegate the translation to the PublishingFieldsTransformer class.

This class will do the actual reading of XML and creating the publishing fields.

So, now the interesting part, the publishingFieldsTransformer class:
The class and constructor look like:
1 internal class PublishingFieldsTransformer
2 {
3 private Microsoft.SharePoint.SPWeb _currentWeb;
4 private const string SPNS = "http://schemas.microsoft.com/sharepoint/";
5 internal PublishingFieldsTransformer(Microsoft.SharePoint.SPWeb currentWeb)
6 {
7 _currentWeb = currentWeb;
8 }
9 }

Nothing special here.
The main entrypoint is the TransformAllFromManifest method:

1 private void TransformAllFromManifest(SPElementDefinition elementsDefinition)
2 {
3 if (string.Compare(elementsDefinition.ElementType,
4 "Module", true, CultureInfo.InvariantCulture) == 0)
5 {
6 XDocument elements = XDocument.Parse(elementsDefinition.XmlDefinition.OuterXml);
7 var module = elements.Root;
8 
9 if (module.Attribute("Url") == null || 
10 string.IsNullOrEmpty(module.Attribute("Url").Value))
11 return;
12  
13 string listUrl = module.Attribute("Url").Value;
14 
15 
16 //process each file
17 foreach (var file in module.Descendants(XName.Get("File", SPNS)))
18 {
19 processFile(listUrl, file);
20 }
21 }
22 }
23

We will loop all files in all modules.

Next we'll process the induvidial files.
1 private void processFile(string listUrl, XElement file)
2 {
3 SPList list = _currentWeb.GetList(
4 SPUtility.GetLocalizedString(listUrl, "cmscore", 1033));
5 
6 //will get the listitem by id, url or name
7 SPListItem listItem = GetListItem(file, list);
8 
9 //if we could not find the listitem we cannot process this file.
10 if (listItem == null)
11 return;
12 PublishingFieldsCheckinHelper.CheckOutItem(listItem);
13 try
14 {
15 foreach (var field in file.Elements())
16 {
17 try
18 {
19 string fieldName = field.Attribute("Name").Value;
20 string fieldValue = field.Attribute("Value").Value;
21 
22 SPField listField = listItem.Fields.GetField(fieldName);
23 
24 if (IsPublishingField(listField))
25 {
26 listItem[fieldName] = GetPublsihingFieldValue(fieldValue
27 , listField);
28 }
29 }
30 catch (Exception e)
31 {
32 //log error
33 }
34 }
35 }
36 finally
37 {
38 listItem.Update();
39 PublishingFieldsCheckinHelper.CheckIn(listItem);
40 list.Update();
41 }
42 }
43

So this is the method doing most of the work. First we use some helper method to find the right list.
Next we check out the listitem, and loop through all fields, using GetPublishingFieldValue for the actual translating.

The last method, as just mentioned is the GetPublishingFieldValue:
1 private object GetPublsihingFieldValue(string fieldValue, SPField listField)
2 {
3 switch (listField.TypeAsString)
4 {
5 case "Image":
6 {
7 return new ImageFieldValue() 
8 { ImageUrl = SPUtility.GetServerRelativeUrlFromPrefixedUrl(fieldValue) };
9 }
10 #region Other Field types
11 #endregion
12 }
13 }
And that's all there is to it!
Now we can just deploy our publishingpages in xml.

So, this xml:
1 <Module Url="$Resources:cmscore,List_Pages_UrlName;" SetupPath="SiteTemplates\SPS">
2 <File Name="Page1.aspx" Url="page1.aspx" 
3 Path="default.aspx" Type="GhostableInLibrary" 
4 IgnoreIfAlreadyExists="TRUE" >
5 <Property Name="Title" Value="My page"/>
6 
7 <!-- This is an pagelayout with an publsihingimage on the left -->
8 <Property Name="PublishingPageLayout"
9 Value="~SiteCollection/_catalogs/masterpage/WelcomeLinks.aspx" />
10 <!-- We can now set this image like an simple url -->
11 <Property Name="PublishingPageImage"  
12 Value="~SiteCollection/PublishingImages/image1.jpg"/>
13 </File>
14 </Module>

Will result in:

Geen opmerkingen:

Een reactie posten