Tuesday, December 11, 2007

Master Page Deployment Through Features

Today I'd like to talk about deployment of master and layout pages in a customized SharePoint site. This is not a new topic, but I am going to focus again on the method relying on feature receivers and the MOSS API.

I needed to deploy a few master and layout pages along with accompanying images and CSS style sheets to the collaboration portal site. The destination for masters, layouts and images was the Master Page Gallery under the root site, and the Style Library for the style sheets. As far as the feature implementation is concerned it does not matter which SharePoint gallery or folder is used for the destination, as long as there is a URL associated with it. So technically I needed to solve 3 problems in order to get the master and accompanying files deployed:

  1. Access the file system directory containing the files to deploy from within the feature receiver;

  2. "Upload" the content of each file to the destination URL;

  3. Check in the changes and publish the uploaded files - I was using collaboration site template which imposed this requirement.
Accessing the file system directory with source files from the feature receiver is easy: during feature activation (and this is when all the work is done in our case) the root folder of the feature physical directory is accessible through the properties.Definition.RootDirectory, where properties is of SPFeatureReceiverProperties type and is made available to the feature receiver.

SPSite thisCollection = (SPSite)properties.Feature.Parent;
SPWeb web = thisCollection.RootWeb;

DirectoryInfo masterSourceFolder = new DirectoryInfo(
Path.Combine(
properties.Definition.RootDirectory,
"MasterPages"));

Uploading the content relies on the SaveBinary() method of the SPFile type to copy file content to the destination gallery or folder. Then the file is checked in and published. Below is an example of a method, which takes a source file system directory and non-recursively copies all files from it to the destination SharePoint folder, either creating new files or updating existing ones.

private void UpdateFolderContent(
DirectoryInfo sourceFolder,
SPFolder destinationFolder)
{
foreach (FileInfo file in sourceFolder.GetFiles())
{
SPFile spFile = null;

try
{
spFile = destinationFolder.Files[file.Name];
}
catch (ArgumentException)
{
//
// Do nothing: WSS throws an exception if a requested
// file is missing from the collection. Ignore it -
// leave spFile equal to null in this case.
//
}

using(FileStream stream = file.OpenRead())
{
if (null != spFile)
{
if (spFile.CheckOutStatus !=
SPFile.SPCheckOutStatus.None)
{
spFile.UndoCheckOut();
}

spFile.UnPublish(UpdateComment);
spFile.CheckOut();
spFile.SaveBinary(stream);
spFile.Update();
spFile.CheckIn(UpdateComment,
SPCheckinType.OverwriteCheckIn);
}
else
{
spFile = destinationFolder.Files.Add(
file.Name,
stream,
true,
UpdateComment,
true);
spFile.Update();
spFile.CheckIn(UpdateComment);
}
}

//
// We do not approve the changes in this feature receiver.
// Apparently the step of checking in a file triggers an
// asynchronous operation in SharePoint, which prevents from
// immediately approving the changes. A workaround is to call
// spFile.Approve() from the new context of a different feature.
//

spFile.Publish(UpdateComment);
}
}

One last task is to approve the changes, otherwise the master and accompanying pages will not be visible to anybody except for the user under whose identity the feature is executing. As stated in the above code example, the same file cannot be both published and then approved within the activation context of the same feature - at least I was not able to find a way to do this. I did find a workaround however - creating a new feature whose purpose would solely be approving the newly published files. The new feature's activation would be dependent on the successful activation of the master and layouts deployment feature. Below is an example of the method which approves all files in a folder within the "content approver" feature receiver.

private void ApproveFolderContent(SPFolder folder)
{
foreach (SPFile file in folder.Files)
{
if (SPFile.SPCheckOutStatus.None == file.CheckOutStatus &&
SPModerationStatusType.Pending ==
file.Item.ModerationInformation.Status)
{
file.Approve(UpdateComment);
file.Update();
}
}
}

To wrap up, it is possible and reasonably straightforward to deploy master pages, layout pages, supporting images and style sheets through features and employ WSS API for this.