In my
previous post I have discussed the rationale behind using features for deployment of SharePoint 2007 based applications and feature receivers for creation of SharePoint sites. Today I will talk about implementation of a feature receiver which creates a SharePoint site.
Feature receiver is a .NET type which inherits from
Microsoft.SharePoint.SPFeatureReceiver and is capable to handle installation, activation, deactivation and uninstallation events by overriding parent's abstract methods. I recommend checking Ted Pattison's MSDN columns for the background on SharePoint 2007
features and their deployment through
solution packages.
Both
FeatureInstalled and
FeatureActivated event handlers are passed an instance of
SPFeatureReceiverProperties object, which provides handy context information about the feature being installed or activated. Not all the properties are available however at a time when
FeatureInstalled handler is called. For example, the following line will not initialize the site collection variable if used from within
FeatureInstalled, but it will work from within
FeatureActivated handler:
SPSite thisCollection = (SPSite)properties.Feature.Parent;For this reason all the API access discussed below is made from within
FeatureActivated handler implementation. I am now going to walk through the scenario of creating a publishing site under root site collection in a given web application.
First of all I need a formal way to convey what exactly needs to be created and configured in my site. I opted to capture the requirements to the site structure inside a custom XML file, which is de-serializable into an object of a custom SiteInfo type. This is how the XML file looks like:
<site
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
url="MySite"
welcomePage="Pages/Page1.aspx">
<pages>
<page id="Page1" url="Page1" layoutFile="SecondaryLayout.aspx">
<zones>
<zone id="IntroText">
<webParts>
<webPart id="wp1" definition="Company.webpart">
<properties>
<properties>
<property name="Title" value="Company Information" />
<property name="ChromeType" value="None" />
</properties>
</properties>
</webPart>
</webParts>
</zone>
<zone id="Graphs">
<webParts>
<webPart id="wp2" definition="Chart.webpart">
<properties>
<properties>
<property name="Title" value="Information" />
<property name="ChromeType" value="None" />
<property name="ChartWidth" value="480" />
</properties>
</properties>
</webPart>
</webParts>
</zone>
</zones>
<connections>
<connection id="ci1"
consumerID="wp2"
providerID="wp1"
consumerConnectionPoint="Field"
providerConnectionPoint="Data" />
</connections>
</page>
</pages>
</site>
As you can see from this snippet, the XML code defines a site in a site collection, a page or multiple pages in the site along with the information about which template is used to create a page, web part connections and zones on each page with web part instances situated inside the webpart zones, and lastly the properties on the webpart instances.
Second of all I need to design .NET types to load site structure from the XML files. In a simplistic form I would have 6 classes related to each other through containment hierarchy:
SiteInfo, PageInfo, ZoneInfo, WebPartConnectionInfo, WebPartInfo, PropertyInfo. All of these would be serializable with XML serialization attributes applied to their properties to control serialization. Here is an example of a
PageInfo type:
[Serializable]
public class PageInfo
{
private List<ZoneInfo> _zones;
[XmlArray(ElementName="zones", IsNullable=true)]
[XmlArrayItem(ElementName="zone")]
public List<ZoneInfo> Zones
{
get { return _zones; }
}
private List<WebPartConnectionInfo> _connections;
[XmlArray(ElementName = "connections", IsNullable = true)]
[XmlArrayItem(ElementName = "connection")]
public List<WebPartConnectionInfo> Connections
{
get { return _connections; }
}
//
// Other properties removed for brevity.
//
public PageInfo()
{
_zones = new List<ZoneInfo>();
_connections = new List<WebPartConnectionInfo>();
}
}
Lastly I need to use
XmlSerializer type to create the SiteInfo objects and all their children by deserializing the above XML file. I will skip implementation details of de-serialization here since it is a well-documented topic. At this point I have all I need to start working with the WSS 3.0 and MOSS 2007 API to create the site with the specified structure.
Before I move on I'd like to answer a question I am anticipating the readers may have: why at all use custom XML and a set of .NET types to describe the site structure when one can apply CAML to achieve the same goal? Indeed there are CAML elements to do similar task, for example one named
AllUsersWebPart capable of defining the web part type properties and target zone on a page. The element became available for use with the features in WSS 3.0. I have had the following reasons about going my way: 1) with my approach a programmer can easily step through all the steps of site creation process in a debugger; 2) it is easy to configure web part connections through the XML file; 3) the entire infrastructure supporting the custom XML file is simple to implement and maintain - it took me about 2 hours to write one. While my approach works, frankly I plan to revisit the CAML notation when I get time - it is the standard, and the more the standard elements are employed - the better.
OK, let's first create a site. The
SPSite object represents collection of all sites including the root site. You can create a new site as follows:
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
// Implementation of GetSiteInfo()is not shown for brevity.
// The site variable of the type SiteInfo contains all site
// structure information loaded from the XML file.
SiteInfo site = GetSiteInfo(properties.Feature);
SPSite thisCollection = (SPSite)properties.Feature.Parent;
string templateName = thisCollection.RootWeb.WebTemplate;
SPWeb web = thisCollection.AllWebs.Add(
site.Url,
site.Url,
site.Url,
DefaultLCID,
templateName,
false,
false);
// Use the new site now...
}
Assuming that the variable named
site contains all required metadata for adding pages and web parts, we can proceed to creating pages, web parts and web part connections. The code snippet below demonstrates how to iterate through the
PageInfo objects, and call methods creating the pages, web parts and the connections.
foreach (PageInfo page in site.Pages)
{
PublishingPage pubPage = AddPageToWeb(
web,
page.Url,
page.LayoutFile);
// SharePoint dynamically assigns IDs to web part instances
// placed on a page. We want to map the assigned IDs to the
// ones defined in the XML file, so that when connecting the
// web parts we can refer to the correct instances.
Dictionary<string, string> mapOfWebPartIDs =
AddWebParts(web, pubPage, page);
AddConnections(web, pubPage, page, mapOfWebPartIDs);
pubPage.CheckIn("New page.");
pubPage.ListItem.File.Publish("New page published.");
}
It is worth to note that the new page needs to be checked in and published in order to avoid manual steps after the feature is activated, therefore the last two steps in the snippet above are to check in and publish each newly created page. The
AddPageToWeb()method finds a page layout by its name, then adds a publishing page to the site. Note that as mentioned earlier, the code example is specific to working with the MOSS publishing site. The code fragment below shows the method implementation.
public PublishingPage AddPageToWeb(
SPWeb web,
string pageFileName,
string pageLayoutName)
{
// Find the page layout to use for the new page
// by iterating through the available layouts and
// picking the right one by name.
PageLayout[] layouts = web.GetAvailablePageLayouts();
PageLayout layout = null;
for (int i = 0; i < layouts.Length; ++i)
{
if (layouts[i].Name.Equals(name,
StringComparison.OrdinalIgnoreCase))
{
layout = layouts[i];
break;
}
}
if (null == layout)
{
throw new ApplicationException(String.Format(
"Cannot find page layout named '{0}'.",
pageLayoutName));
}
PublishingWeb publishingWeb = PublishingWeb.GetPublishingWeb(web);
PublishingPage page = publishingWeb.GetPublishingPages().
Add(pageFileName, layout);
return page;
}
The
AddWebParts() method shown in the following fragment iterates through the zones and web parts defined in the PageInfo object and creates web part instances and sets their properties. It returns back a map between web part IDs as defined in the XML file and actual IDs assigned by SharePoint. For readability purposes implementation of the methods
AddWebPartToPage() and
SetWebPartProperty() is illustrated separately.
private Dictionary<string, string> AddWebParts(
SPWeb web,
PublishingPage pubPage,
PageInfo page)
{
Dictionary<string, string> mapOfWebPartIDs =
new Dictionary<string, string>();
foreach (ZoneInfo zone in page.Zones)
{
int index = 0;
foreach (WebPartInfo part in zone.WebParts)
{
string actualID = AddWebPartToPage(
web,
pubPage.Url,
part.DefinitionFileName,
zone.ID,
index);
mapOfWebPartIDs[part.ID] = actualID;
++index;
foreach (PropertyInfo property in part.Properties)
{
SetWebPartProperty(
web,
pubPage.Url,
actualID,
property.Name,
property.Value);
}
}
}
return mapOfWebPartIDs;
}
The AddConnections() method shown below iterates through WebPartConnectionInfo objects, which reflect the configurations in the XML file, and uses the map between actual and configuration IDs of the web part instances to add connections between the web parts. The AddWebPartConnection() method is illustrated separately.
private void AddConnections(
SPWeb web,
PublishingPage pubPage,
PageInfo page,
Dictionary<string, string> mapOfWebPartIDs)
{
foreach (WebPartConnectionInfo connection in page.Connections)
{
string actualConsumerID = mapOfWebPartIDs[connection.ConsumerID];
string actualProviderID = mapOfWebPartIDs[connection.ProviderID];
AddWebPartConnection(
web,
pubPage.Url,
actualProviderID,
actualConsumerID,
connection.ProviderConnectionPoint,
connection.ConsumerConnectionPoint);
}
}
Where are we? We have created an XML configuration file describing the structure of the new SharePoint publishing site. We have created .NET objects to hold the information from the XML configuration file, we have demonstrated how to add a new publishing site and pages to the site collection programmatically. What's left to do is to look inside of the
AddWebPartToPage(),
SetWebPartProperty() and
AddWebPartConnection() methods. There are some interesting details about using
SPLimitedWebPartManager class to talk about. I will cover these in my next post.