Saturday, November 24, 2007

Add a Web Part to SharePoint Page Dynamically

In my last post I have talked about creating sites and pages with web parts through SharePoint feature receivers. I have left some important details of web part creation out of that discussion. I am going to cover them today.

The method AddWebPartToPage() that was in one of the code fragments in the last post does 2 things: it creates a web part, then adds it to the page into the target web part zone. All manipulations with a web part on a page are governed by the SPLimitedWebPartManager class, which is a scaled down version of SPWebPartManager class designed to do the same thing but without HTTP context available. I think that indeed Microsoft did well here: thanks to this type being available a lot of configuration tasks can be automated, including adding web parts to a page, and connecting web parts to each other. The AddWebPartToPage() method is shown below:

public string AddWebPartToPage(
SPWeb web,
string pageUrl,
string webPartName,
string zoneID,
int zoneIndex)
{
using (System.Web.UI.WebControls.WebParts.WebPart webPart =
CreateWebPart(web, webPartName))
{
using (SPLimitedWebPartManager manager = web.GetLimitedWebPartManager(
pageUrl, PersonalizationScope.Shared))
{
manager.AddWebPart(webPart, zoneID, zoneIndex);
return webPart.ID;
}
}
}
The CreateWebPart() method first constructs a CAML query, which will look up the web part in a web part gallery. Certainly this means that for the method to work the web part must already be in the web part gallery. Well this is easy to do - the site creation feature within the context of which we are operating should depend on a feature installing the web part we intend to use. The dependency can be specified in the feature manifest file using ActivationDependency element. When I was specifying the web parts in our XML configuration file (see my last post) I chose to use the web part definition files (the ones with .dwp or .webpart extensions). These files are created for custom web parts manually or automatically if you have Visual Studio WSS 3.0 Extensions installed, or for the out-of-the-box web parts, which are a part of SharePoint installation they can be found here: %ProgramFiles%\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES. Getting back to the CAML query though, it returns the web part gallery items whose definition files match the passed parameter. The field used to filter the query is FileLeafRef. Next the type and assembly of the web part are determined from the other fields of the gallery item - the WebPartTypeName and WebPartAssembly and the instance of the web part is created through reflection. Here is complete implementation of the method:

private System.Web.UI.WebControls.WebParts.WebPart
CreateWebPart(SPWeb web, string webPartName)
{
SPQuery query = new SPQuery();
query.Query = String.Format(
"<Where><Eq><FieldRef Name='FileLeafRef'/><Value Type='File'>{0}</Value></Eq></Where>",
webPartName);

SPList webPartGallery = null;

if (null == web.ParentWeb)
{
// This is the root web.
webPartGallery = web.GetCatalog(
SPListTemplateType.WebPartCatalog);
}
else
{
// This is a sub-web.
webPartGallery = web.ParentWeb.GetCatalog(
SPListTemplateType.WebPartCatalog);
}

SPListItemCollection webParts = webPartGallery.GetItems(query);

string typeName = webParts[0].GetFormattedValue("WebPartTypeName");
string assemblyName = webParts[0].GetFormattedValue("WebPartAssembly");
ObjectHandle webPartHandle = Activator.CreateInstance(
assemblyName, typeName);

System.Web.UI.WebControls.WebParts.WebPart webPart =
(System.Web.UI.WebControls.WebParts.WebPart)webPartHandle.Unwrap();
return webPart;
}
On to SetWebPartProperty() method. In the XML configuration file the property element only has name and value attributes. When setting a property programmatically the property to be set is looked up by its name using reflection, then its value obtained from the value attribute is converted to the type of the property and assigned to it. I do not list the implementation of ConvertValue() method here - it can be either trivial or quite complex, depending on the need. I have opted to switch between the expected property types and throw an exception for unexpected ones. This solution easily handles properties with the intrinsic value types and enumeration types. The SetWebPartProperty() method implementation is shown in the code fragment below:

public void SetWebPartProperty(
SPWeb web,
string pageUrl,
string webPartID,
string propertyName,
string propertyValue)
{
using (SPLimitedWebPartManager manager = web.GetLimitedWebPartManager(
pageUrl, PersonalizationScope.Shared))
{
System.Web.UI.WebControls.WebParts.WebPart part =
manager.WebParts[webPartID];
Type runtimeType = part.GetType();
PropertyInfo property =
runtimeType.GetProperty(propertyName);
object value = ConvertValue(propertyValue, property.PropertyType);
property.SetValue(part, value, null);
manager.SaveChanges(part);
}
}
Finally all what's left is connecting the web parts. Again SPLimitedWebPartManager comes at help here. The method AddWebPartConnection() first locates consumer and provider web parts by their unique IDs, which were captured earlier at a time of placing the web parts on a page; next it iterates through provider and consumer connection points to find the ones specified in the XML configuration file. By the way, these are the same as the constructor parameters used with ConnectionProviderAttribute and ConnectionConsumerAttribute types, which are used to decorate methods participating in a web part connection. When both connection points are determined we can call the SPConnectWebParts() method of SPLimitedWebPartManager to create the connection. The method implementation is shown below:

public void AddWebPartConnection(
SPWeb web,
string pageUrl,
string providerWebPartID,
string consumerWebPartID,
string providerConnectionPointName,
string consumerConnectionPointName)
{
using (SPLimitedWebPartManager manager = web.GetLimitedWebPartManager(
pageUrl, PersonalizationScope.Shared))
{
System.Web.UI.WebControls.WebParts.WebPart provider =
manager.WebParts[providerWebPartID];
System.Web.UI.WebControls.WebParts.WebPart consumer =
manager.WebParts[consumerWebPartID];

ProviderConnectionPointCollection providerPoints =
manager.GetProviderConnectionPoints(provider);
ConsumerConnectionPointCollection consumerPoints =
manager.GetConsumerConnectionPoints(consumer);

ProviderConnectionPoint providerPoint = null;

foreach (ProviderConnectionPoint point in providerPoints)
{
if (String.Equals(
providerConnectionPointName,
point.DisplayName,
StringComparison.OrdinalIgnoreCase))
{
providerPoint = point;
break;
}
}

ConsumerConnectionPoint consumerPoint = null;

foreach (ConsumerConnectionPoint point in consumerPoints)
{
if (String.Equals(
consumerConnectionPointName,
point.DisplayName,
StringComparison.OrdinalIgnoreCase))
{
consumerPoint = point;
break;
}
}

manager.SPConnectWebParts(
provider,
providerPoint,
consumer,
consumerPoint);
}
}
One last note: when putting the code fragments together I was removing the "defensive programming logic" such as checking for nulls or throwing exceptions on unexpected values - this way the examples became less cluttered. Any practical implementation of the ideas described in this and previous posts therefore would require creative refactoring of these examples. I hope though I managed to save you some time that I spent looking through SDK documentation, blogs and decompiling the source code...

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.

Tuesday, November 20, 2007

Using Feature Receivers to Automate Site Deployment in MOSS 2007

I was inspired by Chris O'Brien's great discussion of feature-based deployment when I started looking at using features myself. I am guessing that the situation which I was in when a web application based on SharePoint had to be fully delivered by development team without reliance on the site masters or the end users to do its configuration is not that uncommon. While the extensive configurability of SharePoint is undoubtfully its essential strength, it also creates practical challanges with regards to development and deployment of custom applications based on SharePoint if these applications also assume minimal or no configuration to be carried out by non-developers.

When a SharePoint-based application is being constructed a lot of things can be configured instead of being coded from scratch as they would be in a custom application, and this is certainly a good thing. The problem is this creates a potential for configuration errors, which are harder to debug than coding errors because you cannot simply step through them in a debugger to determine what went wrong. Another concern is how to ensure proper source control of the pieces of functionality implemented through a series of configuration steps of a SharePoint-based site rather than through the coding. Fortunately WSS and SharePoint expose a powerful API which allows automating the numerous configuration steps. Once configuration steps are automated the chances for configuration errors as well as deployment time reduce greatly, and the functionality which depends on the configuration can now be effectively source controlled.

I have been facing these challenges when I needed to streamline build and deployment process of a web site, which was based on MOSS 2007 Publishing Site template and was in its final development stage. Custom site definition was not used originally, instead a fixed number of pages were added manually to the root site and sub-sites, web parts were placed on the pages, and their properties configured by developers. So here was my challenge: how to deploy the site avoiding repetition of the manual configuration steps each time and how to source control the pages? I have eventually decided to rely on the features, and feature receivers in particular to do what I needed. In the following posts I intend to discuss my approach to automating creation of sub-sites in a site collection, automating page creation, adding web parts, setting their properties and web part connections.

Saturday, November 10, 2007

Hello World

Hi there!
I've been thinking to start blogging for a while and experience it all for myself. Finally here I am. I've been doing software consulting focusing on .NET development for several years and believe that I have got a few things to share with people. Stay tuned and hope you will find my blog interesting.