What is Web Content Management?

Although many peope are familiar with the collaboration features of SharePoint, the web content management featurs are not as often so well-known. Before you start thinking about using SharePoint to publish content to the Web, it can be helpful to first ask the question, "What is web content managment, after all?" I've put together a quick 10 minute MP3 audio recording that answers that basic question, as well gives a brief explanation of why SharePoint is a good WCMS. You can also download the transcript of the audio session.

What is a WCMS?
Format: MP3
Length: 9:21
Size: 3.75 MB

What is a WCMS? transcript
Format: Word Document
Size: 48 KB

Best Practices Conference Redux
Back by popular demand, Bill English and company are hosting another SharePoint Best Practices conference in February, but this time on the west coast, in San Diego. I just sent in my registration and am looking forward to meeting other SharePointers. Let me know if you're planning on attending and would like to meet!
Intro to MOSS WCM Presentation
Thank you to all of you who showed up at today's St. Louis SharePoint User's Group meeting. I enjoyed meeting some new people this month. For those who would like copies of the presentation assets for themselves, here are links to the presention slide deck, as well as the code I used to provision my Page Layout as a feature.
 
 
Enjoy!
Updating Content Types and Site Columns That Were Deployed as a Feature
How many times have you come across this scenario? You create several Site Columns as well as a Content Type or two in your Feature. Then you want to update it. Since you deployed it via a Feature (as opposed to editing it in the browser), being a good developer, you decide you want to execute your update through a Feature as well. However, you run into several problems:
  • When you deactivate your Feature, it keeps some Site Columns and deletes other ones.
  • You notice some of the Site Columns and Content Types are still hanging around in the lists you applied them to, even after you deactivated your Feature.
  • You try doing an in-place upgrade of your Feature, and nothing happens; everything looks exactly like before. The columns in your Content Type haven't changed or anything.

This can be very confusing and aggravating, and this is a scenario I've walked through with several clients. Why does this happen? And what should be a proper approach for upgrading a Feature that includes Site Columns or Content Types?

Site Columns and Content Types in List
When you add a Content Type, or a Site Column, to a list, SharePoint actually makes a copy of that item in the list. However, the copy seems to retain the same GUID/Identifier as the original item. This allows several things:

  • This acts as a "delta" of sorts; when you add a Site Column to a list you can say, "I want to use this Site Column, except in this list, I want it to display 7 lines of text instead of 6," etc. By having a copy, SharePoint can modify the properties of your Site Column or Content Type in your list, while keeping it separate from the "original" version in your Site Collection root. The same applies for Content Types: what if you wanted to attach a Workflow to your Content Type, just for this list? It needs a copy of that Content Type in that list.
  • Have you noticed how, when you're modifying a Site Column or a Content Type, you have that little check box that asks if you want to push your change down to everything that uses it? You have two choices: either you will, or you won't. If you don't want to apply your changes to existing items, how would SharePoint handle that? Well, it essentially leaves the existing copies of the Site Columns that are applies to lists alone, and just uses your changes when creating new copies of that Site Column in the future. By using a copy of the Site Column in lists, it's possible to have multiple definitions for a Site Column being used at once.
  • If you decide you do want to apply your changes to everything that uses that item, how does SharePoint do it? Essentially, it looks for any Site Column or Content Type in the site collection hierarchy that is using the same GUID (or ID, the case of a Content Type). That provides a sort of implicit connection between the two. (Think of it like siblings: they're different people, but they have the same last name and the same parents.)

Deactivating Your Feature
What happens when you deactivate your feature? Typically, if a Site Column or Content Type is not being referenced in any list or anywhere else in the site, SharePoint will remove it from the Site Collection. However, if it's being used, SharePoint will often delete the ones that aren't be used, but leave the ones that are. On occasion, deactivating the Site Columns will actually remove the items. However, the copies of the assets will remain in the underlying lists. This might seem confusing at first, but it's because the lists contain copies. (Using our analogy again, if siblings' parents go on vacation, the siblings still retain their last name and their sibling relationship, whether the parents are around or not.)

I'd like to add a little caveat here: I have run into many issues with SharePoint when it comes to modifying Site Columns and Content Types using Features, whether it means changing the Site Column type, changing the composition of Content Types, etc. Because SharePoint is keeping track of multiple versions of these assets, you can easily get it confused, especially if you try to change the very field type of a Site Column. The purpose of this blog entry is to make this easier for you. However, I strongly recommend you have a clean development environment to work on, since once you run into a mismatch in show SharePoint handles its Site Columns or Content Types, it has the possibility of disrupting operations for your whole Site Collection.

Activating Your Feature
If we think about it in reverse, what happens when we activate a feature? SharePoint uses the CAML markup we used to define our Site Column or Content Type, and it puts it into the database. This is a one-type event. What do I mean by that? If you create a Master Page via a Feature, you do put a reference to that Master Page in SharePoint, so that you get your new Master Page showing up in the Master Page Gallery for your site. However, all that reference does is point to the file system. Any time SharePoint to use that Master Page, it says, "Hmm, looks like I should look at the file on the file system in the Features folder instead of a file stored in my database." However, with Content Types, once the Feature has been activated, SharePoint uses the CAML to add the Site Column or Content Type to its database, and it never uses that asset on the file system again. Basically, it's the same as using a Feature Receiver that executes code once when the Feature is activated, and never again. (Please note: I have no inside information from Microsoft about this! This is just what I've inferred based on the behavior of these assets.)

Creating Site Columns and Content Types Using a Feature Receiver
So this begs the question, why not just use a Feature Receiver that executes code which instantiates these things into SharePoint, and just forget the CAML markup altogether? Well, remember what we were saying about the relationship between GUIDs in the lists and their parent items in the Site Collection? It's often important that if you're activating and deactivating a Feature in a Site Collection that the GUIDs remain the same, so-as to prevent orphaning those assets in the child lists. Furthermore, if you're activating the Feature in multiple Site Collections, you probably want continuity between the assets in different Site Collections.

When you create a new Site Column via code, using the SiteColumnCollection.Add() method, SharePoint automatically creates a GUID for that Site Column. The only way you could hard-code that GUID would be to create the Site Column by passing in the entire XML definition. Well, if you're going to do that, you might as well just use the CAML way of doing things. And what about Content Types? I looked, and I don't even see a way of specifying the unique identifier for a Content Type. It's true, you can pass in the parent Content Type, and the way SharePoint constructs ID's for Content Types, its ID includes the ID of its parent. However, I have found of now way of passing in what the newly-created Content Types unique part of the ID should be. (If you're confused about what I'm talking about when referring to Content Type IDs, take a look here: http://msdn.microsoft.com/en-us/library/aa543822.aspx.)

Recommended Approach
Based on all these factors, my suggestion is this:

  • Create two Features: one for the original markup and one for making changes. (Or you can put them in the same Feature; I just want to differentiate between where you do what.)
  • The original Feature should contain the CAML for Site Columns and Content Types. This ensures the IDs have been assigned ahead of type and remain constant.
  • If you want to update a Site Column by changing nearly anything about it except its Field type, do it using a Feature Receiver. By doing this, you can call the Update method and pass in a boolean indicating if you want all the existing assets in the site that inherit from this to update to, (something you couldn't do via the CAML.)
  • You can also add an existing Site Column (that you provisioned via the CAML feature) to an existing Content Type (that was provisioned via the CAML feature). This is helpful if the Column was not part of that Content Type before, etc.
  • In a scenario like the one I just mentioned in the last bullet point, it's necessary to deactivate and reactive the CAML feature (to provision the new assets) before calling your Feature Receiver. What will this mean for the site? Since all the Site Columns and Content Types in the lists in the site are using the same ID's as the ones provisioned in the Site Collection root, removing its parent from the Site Collection won't change that. It might leave it orphaned temporarily, (i.e. there will be no relationship between that item and an item in the Site Collection root, but it will function the same way it always has, since it's really a fully-functioning copy of the original item) until you reactivate the Feature that puts the item back in the Site Collection. It's like the parents are going on vacation when you deactivate the Feature, and are coming back home when you activate the Feature again.

You have a choice when it comes to how you maintain the CAML and the Feature Receiver, since you have two scenarios: existing Site Collections and new ones.

  • You could make a policy that every time you write code in your Feature Receiver to update a Site Column or Content Type, you have to make the change in your CAML as well. That would mean that every time you activated the CAML Feature in a "fresh" Site Collection, the CAML would be up-to-date and accurate; there would be no need to run the "updater" feature. (In your Feature Receiver, you should make sure you do some extra checking to make sure a Site Column doesn't already belong to a Content Type before adding it, etc. in case that change is already in place before the code executes.) This approach means you only have to execute one Feature when creating a new Site Collection, but it also means you're maintaining changes in two places: in your Feature Receiver for making changes to existing sites, and in your CAML for new sites. It's a cleaner approach, but also contains an element of redundancy, which always leaves room for human error.
  • The other approach is to simply assume that every time the base CAML feature is activated, you're always going to execute the Feature Receiver. This approach says the only time you'd change the CAML is to add a new Site Column or new Content Type; otherwise, all the changes happen in the Feature Receiver. This approach reduces redundancy, but also means your Feature Receiver code could get quite large with all your changes over time, and it could leave your CAML as very much "legacy" over time.

Deleting Site Columns and Content Types
If you've noticed, I haven't written much about deleting Content Types and Site Columns. That's because deleting a Column could easily mean deleting the content stored in that Column. As we've already noted, deleting a Site Column won't delete the copies of that Site Column from lists in the site that use it. If you want to delete a Column from a list, you'll need to write code that will explicitly allow this, since it means you'll also lose any content that was stored in that column in the list. You'll have to explicitly set a property called AllowDeletion on the SPField object you're trying to delete.

In terms of deleting a Content Type from a site, the same thing goes, in terms of deleting a Content Type from the Site Collection root won't necessarily remove the copies of that Content Type in lists. Also, removing a Content Type from a List after the Content Type has been removed from the Site Collection won't necessarily remove the Columns from the list, if it now thinks those Columns are just part of its own definition, and not a Content Type's columns.

In my experience, this is where things can get hairy, since you're dealing with copies without parents, etc. It's imperative that you do things in the right order, and practice in a development environment before trying to execute this sort of scenario in production.

Conclusion
Dealing with the issue of updating Site Columns and Content Types in SharePoint can be a tricky issue when using Features, but hopefully this will provide you some guidance about how to carry this out in your own environment.

InfoPath and Web Content Management: an End-to-End SharePoint Scenario -- Part 4: Creating the Workflow

Part 1: Creating the InfoPath Form
Part 2: Submitting the InfoPath Form to a Document Library
Part 3: Creating the Page Layout

In this series, I'm taking you through the step-by-step process of creating content in an InfoPath form that can be published on the Web. To recap where our series has taken us so far, we have:

  • Created an InfoPath form that allows users to enter information about their newborn baby.
  • Created the ability for the informatin from the form to be saved to a SharePoint document library when the form is submitted.
  • Created a Publishing Page Layout that allows informaion about the newborn to be published to an external site.

Today I'll be showing you the last step in the process, which is the glue that holds it all together: a custom workflow that will create a Publishing Page in the Pages Library, using the Page Layout we created in the last step, which will contain the information that was collected from the InfoPath form. In this example, I'll be writing my code in Visual Studio 2008 using C#.

Creating the Wokflow Project in Visual Studio
I've installed the Visual Studio Extensions for Windows SharePoint Services (called VSeWSS for short.) VSeWSS has been published for both VS 2005 and VS 2008, and contains a set of pre-fabricated Visual Studio templates for you to use, such as creating a SharePoint web part, a SharePoint list, etc. Today I'm going to be using the SharePoint Sequential Workflow project template.

The first step is to open up Visual Studio and say you want to create a new project. We'll name it "InfoPathPublishingWorkflow".

On the next screen, a dialog will ask you which library you want to debug your workflow on. You can choose the Cradle Roll document library where you saved your InfoPath forms.

Once you've created your new project,  rename your "Workflow1" class to "InfoPathPublishingWorkflow". Click "OK" to the pop-up box that asks if you also want to update all references to "Worklow1" in your code.

You'll see a workflow diagram that looks like this:

If you notice, there's a red exclamation point beside the "onWorkflowActivated1" activity. That's because there's still a reference to Workflow1 lying around. Click on the activity and view the Properties window, and expand the "CorrelationToken" field. You'll see it still says "Workflow1". Click on Workflow1 and you'll see it's a drop down menu. Select "InfoPathPublishingWorkflow" from the menu, and you'll see the red exclamation points disappear. Also change the "Name" property under "WorkflowProperties" to "InfoPathPublishingWorkflow" as well. Open up the workflow.xml file and change the reference to Workflow1 there, too. Last but not least, right click on "InfoPathPublishingWorkflow.Designer.cs" and say "View Code". Do a search for "Workflow1" and replace it with "InfoPathPublishingWorkflow".

We're going to be referencing Publishing objects in our code, so we'll need to add a reference to the SharePoint Publishing dll to our Visual Studio project. Browse to the "ISAPI" folder in your "12 hive". Typically, the path is "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\ISAPI". Select the dll called "Microsoft.SharePoint.Publishing.dll".

The next step we're going to take is to drop a "Code" activity into our workflow. All we're doing is saying, "Once our workflow starts, do some stuff." Open up your Toolbox in Visual Studio, find the "Code" activity, and drop it on the Workflow Designer, directly following the onWorfkowActivated activity. For the sake of readability, we'll rename it "createPage". Double click on the activity in the designer, and it will create a method that we can use.

Go to the top of the page and add a "using" reference to Microsoft.SharePoint.Publishing and Microsoft.Sharepoint.Publishing.Fields. I'm going to walk you step-by-step through the code we're going to use to create the publishing page.

try
{
  using (SPSite site = this.workflowProperties.Site)
    {
      using (SPWeb rootWeb = site.RootWeb)
      {
      }
    }
}

To start with, we're going to wrap everything in a try/catch block for easier debugging. Then we're going to use the "using" blocks for better garbage collection. Inside the "using" blocks, add this:

PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(rootWeb);
PageLayout[] layouts = pubWeb.GetAvailablePageLayouts();
for (int i = 0; i < layouts.Length; i++)
{
  PageLayout layout = layouts[i];
  if (layout.Title == "Cradle Roll")
  {
  }
}

The first thing we're going to do is use the Publishing API to convert the SPWeb object to a PublishingWeb object. We do this by calling the "GetPublishingWeb" method of the PublishingWeb object, and passing in our SPWeb object. (For the sake of our example, all our lists and libraries are stored in the site collection's root web site.) Next, we're going to loop through all the available Page Layouts on the site, looking for our "Cradle Roll" page layout, which we'll use to create a new publishing page.

SPListItem workflowItem = this.workflowProperties.Item;

string pageName = workflowItem["Babys_x0020_First_x0020_Name"].ToString() + workflowItem["Babys_x0020_Last_x0020_Name"].ToString();

SPQuery newQuery = new SPQuery();
newQuery.Query = "<Where><Eq><FieldRef Name='Title'/>" +
"<Value Type='Text'>" + pageName + "</Value></Eq></Where>";
SPListItemCollection pages = pubWeb.PagesList.GetItems(newQuery);

The first thing we're doing is creating a local reference to the list item that the workflow is executing against. Since we're going to be attaching our workflow to the Cradle Roll document library, the "workflow properties item" will be a list item in that library.

Next, we're coming up with a unique name for our Publishing Page in the Pages library. Granted, concatenating first and last name is not real unique, but it works for this example. Feel free to append other distinguishing information to ensure unique pages get created for each item in your document library. (In this example, if a page with the same name already exists, this workflow will overwrite it with new information.)

The next thing we're doing is creating a CAML query using the SPQuery object. (CAML stands for "Collaborative Application Markup Language", and is a way of querying the SharePoint database directly for items, using XML-style notation.) We're querying the Pages library to see if a page already exists for this particular baby.

try
{
  cradleRollListItem = pages[0];
  if (cradleRollListItem.File.CheckOutStatus == SPFile.SPCheckOutStatus.None)
  {
   cradleRollListItem.File.CheckOut(); 
  }
  else
  {
    return;
  }
}
catch
{
  PublishingPage cradleRollPage = pubWeb.GetPublishingPages().Add(pageName + ".aspx", layout);
  cradleRollListItem = cradleRollPage.ListItem;
}

Unfortunately, if the query doesn't return any results, your result set's count won't be set to 0, it will simply be null. Since it's null, if you try to query the "Count" property of a "null" object, it will throw an error. We're just incorporating this into our code. If we don't get an error, we're assuming results were returned, so we'll use the first page we get back. Otherwise, we're going to create a new PublishingPage object, giving it a page name and a Page Layout to use, and adding it to the Pages library. In both cases, we're setting a local reference to that Page's SPListItem object that we can use later for setting its field values.

cradleRollListItem["Title"] = pageName;

SPContentType cradleRollContentType = rootWeb.ContentTypes["Cradle Roll"];

foreach (SPField siteColumn in cradleRollContentType.Fields
{
  if (siteColumn.Title.StartsWith("Baby") || siteColumn.Title.StartsWith("Father") || siteColumn.Title.StartsWith("Mother"))
  {
     if (siteColumn.Title != "Baby Photo 1" && siteColumn.Title != "Baby Photo 2")
     {
       cradleRollListItem[siteColumn.Title] = workflowItem[siteColumn.Title];
     }
     else
     {
       string babyPhotoUrl = GetBabyPhotoUrl((int)workflowItem[siteColumn.Title], pubWeb.Web);
       if (babyPhotoUrl != "")
       {
         string fieldName = siteColumn.Title.Replace("Photo", "Image");
         ImageFieldValue babyPhotoImageFieldValue = cradleRollListItem[fieldName] as ImageFieldValue;
         if (babyPhotoImageFieldValue == null)
         {
           babyPhotoImageFieldValue = new ImageFieldValue();
         }
         babyPhotoImageFieldValue.ImageUrl = rootWeb.ServerRelativeUrl + babyPhotoUrl;
         cradleRollListItem[fieldName] = babyPhotoImageFieldValue;
       }
     }
   }
}

 This is a bigger chunk of code, so let's go through it a little at at time. First, we already know the title of the page, so let's set it. Secondly, we're going to iterate through the fields that are a part of the "Cradle Roll" content type to copy values from the InfoPath library to the Pages library, since we're using virtually the same columns from that content type in both places. We only want to copy the relevant fields, so to skip all those additional fields like "last modified by", etc., I'm only looking to copy those fields that start with words like "Baby", "Mother", and "Father".

If you remember correctly, we only stored the ID of the item in the Baby Photos image library with the InfoPath library list item. We need to execute some special code to look at the image library and retrieve the URL, so we can populate the Publishing Image field. Here's the private method we're calling to do that:

private string GetBabyPhotoUrl(int id, SPWeb web) {
  try
  {
    string url = "";
    if (id != 0)
    {
      SPList babyPhotos = web.Lists["Baby Photos"]; SPListItem babyPhotoItem = babyPhotos.GetItemById(id);
      url = babyPhotoItem.File.Url;
    }
    return url;
  }
  catch
  {
    throw;
  }
}

Last but not least, we're going to update our newly-modified page list item, and check it in.

cradleRollListItem.Update();
cradleRollListItem.File.CheckIn("Created by InfoPath Publishing Workflow.");
break;

Deploying the Solution
One of the virtues of using the Visual Studio template for workflows is that you can easily deploy the project. To do so, right click on your project, and click "Build" to build your project, then "Deploy" to deploy your project to the server.

If you didn't set up a workflow association when you first created your project, you can go to your Cradle Roll list and explicitly associate this workflow with that document library. To do so, go to the List Settings page for your Cradle Roll document library, and click on "Workflow Settings." If you click on "Add a workflow", you should now see "InfoPathPublishingWorkflow" listed under the other SharePointn workflows, as a kind of workflow you can create.

There you go! Give it a whirl! Try kicking off your workflow, and you'll see a new Page appear in your Pages library, which should have the information from the InfoPath form, along with the pictures you specified.

Congratulations! You're done!

Click here to download InfoPathPublishingWorkflow.cs as a text file.

1 - 5 Next