Wednesday, November 21, 2007

Create a Site Using Feature Receiver and SharePoint API

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.

7 comments:

  1. Ivan this was a great post. Have you continued to use this approach? Have you posted the solution code?

    Thanks... C.

    ReplyDelete
  2. Hi Charlie,
    As I keep on working with SharePoint, I see a lot of need for this. And also I see that companies experienced with SharePoint have their own versions of what I was posting on - extensive use of SharePoint API for deployment tasks. As for the code base of solution - I am bound by an NDC, but I may be able to help if you have specific questions.
    Ivan.

    ReplyDelete
  3. HI,

    "The AddPageToWeb()method finds a page layout by its name, then adds a "publishing" page to the site." (the code example is specific to working with the MOSS publishing site)

    I want to do the same thing in WSS 3.
    Please help me.
    Sumudu.

    ReplyDelete
  4. Hi ,

    I recently started SharePoint and I want to create a site configurator, your post is very helpfull.

    I shall be thankful if Can you show me the implementation of GetSiteInfo() method.

    Thanks.

    ReplyDelete
  5. Hi Sumudu,
    there you go - here is the GetSiteInfo() method implementation. As you see you need to first have the SiteInfo class with related PageInfo and other classes. Then the method simply deserializes it from XML:

    private SiteInfo GetSiteInfo(SPFeature feature)
    {
    string structurePath = Path.Combine(
    feature.Definition.RootDirectory,
    ((SPFeatureProperty)feature.Properties[
    SiteStructureRelativePathKey]).Value);

    XmlReaderSettings settings = new XmlReaderSettings();
    settings.CloseInput = true;

    using(FileStream stream = File.OpenRead(structurePath))
    using (XmlReader reader =
    XmlTextReader.Create(stream, settings))
    {
    XmlSerializer serializer = new XmlSerializer(typeof(SiteInfo));
    SiteInfo siteInfo = serializer.Deserialize(reader) as SiteInfo;

    if (null == siteInfo)
    {
    throw new ApplicationException(
    "Null value or unexpected type was deserialized.");
    }

    return siteInfo;
    }
    }

    ReplyDelete
  6. Regarding the same code for WSS 3.0 site - you cant because publishing features are not available unless you install SharePoint Server. Your best bet is creating a page in the Pages document library - unfortunatly I don't have an example for WSS.

    ReplyDelete
  7. Hi Iven, Thanks for the reply.
    I have successfully Integrated Reporting Services with Sharepoint (installing Report Services for sharepoint add-in). And I want to programatically using OM create an instance of "ReportingViewer.dwp" webpart from the Web part Gallery (which is installed by means of the add–in ), I'm using the CreateWebPart() Method for that. But my issue is this method "CreateWebPart()" works with anyother web part except the "ReportingViewer.dwp".

    I mean it creates the webPart instance but I cannot add that to the WebpartManager, It says "Ivavalid XML format" then I expole the created instance form QuickWach" Debugger tool, there is a Invalid Operation exception in the "Tile" Property.

    How can I create an Instance of the "reportViewer.DWP" ? What seems to be the problem. ?
    Please Help.
    Thanks

    ReplyDelete