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...

6 comments:

  1. Hi there,

    I was wondering if it´s possible to do what you´ve done above but in a declarative way in an application page under the _layouts folder. In other words: Static pages (pages not created using MOSS) referencing web parts (provider and consumer) and a connection between them to exchange data.

    Thanks

    Fábio Akira Yoshida

    ReplyDelete
  2. Hi Fabio,
    You should be able to connect 2 web parts on an ASP.NET page running in _layouts folder. The web part connections will work without SharePoint: http://msdn.microsoft.com/en-us/library/ms178188.aspx"

    As this article describes, you can do it either declaratively or programmatically.

    ReplyDelete
  3. Thanks for this code.
    Here is an implemetation for ConvertValue which will cover a large set of conversions:

    if (propertyValue != string.Empty)
    {
    object value = Convert.ChangeType(propertyValue, property.PropertyType);
    }

    ReplyDelete
  4. Thanks a lot code really helped me.

    ReplyDelete
  5. Thanks Ivan - now linked to Stack Overflow.

    http://stackoverflow.com/questions/2420596/how-do-i-enumerate-web-part-types-in-wss-site/2424295#2424295

    ReplyDelete
  6. Thanks a lot, this was very helpful!

    ReplyDelete