Thursday, 19 January 2012

WebPartAdderExtension – a better way of deploying web parts

Recently, that nice young man Wictor WilĂ©n blogged about WebPartAdderExtension and web part gallery sources – this is a little-known (and currently undocumented) capability in SharePoint which allows you to deploy custom web parts in a different way, with huge benefits. In summary, SharePoint 2010 provides a means for you to specify web parts to appear in the web part ‘picker’, without requiring a .webpart file in the local web part gallery (which is the usual route). Code is required, but SharePoint 2010 provides a ‘hook’ for you to add this in the form of the WebPartGallerySourceBase class. To help visualize this, the web part highlighted in the image below is deployed using WebPartAdderExtension – it appears just like a custom web part which uses a .webpart file and the user is blissfully unaware of any differences:

WebPartAdderExtension_WebPartSelection

I read Wictor’s two excellent articles, and on a recent project I decided to extend his sample into a model which I think works well for many scenarios. Now, I don’t want to be too dramatic, but I think WebPartAdderExtension in general is A Big Deal – I just can’t imagine going back to the old way of doing web parts at all. In this article we’ll first have a recap of what the problem is and why this new way is better, and I’ll then talk about my implementation and where you can download the files.

The problem

In a nutshell, web parts and the web part ‘gallery’ model in SharePoint are great, until you have to make a change to the definition of a web part that’s already out there. This usually takes the form of:

Client: “Hey can we change the title of ‘foo’ web part? Actually that name we came up with isn’t great and users aren’t finding it.”
Client: “Hey can we move ‘foo’ web part into a different category? That’s a quick change right?”

As a developer you say “Sure, I’ll get right on it!”. But then you realise that it’s deployed to everyone’s My Sites. And you have thousands of them. Or maybe it’s in team sites, but come to think of it you have quite a few of those too. This matters, because the web part is in each site collection’s web part gallery (in the .webpart file), and you’ll have to perform some operation to iterate every site and do some sort of processing on each one. Depending on how many sites you have to deal with, this could take could take significant time to run, and will place a certain amount of load on the infrastructure whilst it does. On my last project, it usually took around 7-8 hours to iterate the 25,000 My Sites we had, and we could only run it during a long maintenance window due to the load on both the server doing the processing and the database servers. Of course, 25,000 sites isn’t that big. In larger enterprises, I’ve even heard of folks writing custom multi-threaded applications to deal with such “iterate the sites” scenarios. I’m not saying you won’t need to occasionally perform this iteration for other reasons, but if we could take one common use case (i.e. changes to web part definitions) out of the equation, that’s got to be good in my book.

The solution

Consider that with WebPartAdderExtension, rolling out such a change to (say) 25,000 sites has the following characteristics:

  • No iteration required, therefore no load on servers
  • Takes immediate effect across all sites
  • Depending on your implementation, may not even require an app pool recycle!

How it works: WebPartAdderExtension is a Feature element (XML), and this can be defined at farm/web app/site collection scope (which is interesting in itself!). This ‘points’ to a class which derives from WebPartGallerySourceBase and in here you can do what you like – all you have to do is return a collection. Each item in the collection will then appear as a web part in the web part gallery. Your custom code is fired when a user browses for a web part (in page edit mode). In Wictor’s sample, he fetched items from a SharePoint list, meaning the pseudo web parts were actually list items rather than each having a traditional .webpart file.

 My implementation

Wictor’s sample conveyed the principle to me perfectly, but I ended up with a different model. Here’s what I came up with to meet our requirements:

  • A series of XML files, which define web parts by web application (e.g. an XML file for ‘teams’ web parts, an XML file for ‘My Site’ web parts, and so on).
    • N.B. It could be the nature of our web parts, but by web app was definitely the most appropriate factoring for us.
  • Code which reads the XML file for the ‘current’ web application
  • A web application-scoped Feature, which uses WebPartAdderExtension
    • In fact, several variations of this Feature are created, one per web app. The ‘teams’ web parts Feature uses WebPartAdderExtension to point to the class which knows about ‘teams’ web parts – this in turn reads from the ‘teams’ web parts XML file.

So rather than Wictor’s list-based sample, I’m happy to use XML files on the filesystem – when we specify the web parts which can be picked, we must specify the assembly/type name etc., and since this is in developer territory I’m comfortable with the fact that a WSP deployment is required to update this XML (the files live under ‘_layouts’). Somewhat surprisingly, SharePoint doesn’t cache this data so even in my file-based implementation, an app pool recycle is not required – just update the WSP (and ensure ResetWebServer = false) and the changes take effect immediately. Even if it DID require a recycle, remember this is still several million times better than a long-running process to iterate site collections.

Here’s my XML (custom schema) for defining the set of ‘teams’ web parts (using dummy web parts here, in my COB namespace): 

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <WebParts>
   3:   <WebPart
   4:     Title="My web part for team sites - deployed via WebPartAdderExtension"
   5:     Category="COB"
   6:     Description="A dummy web part for team sites"
   7:     ID="TeamsWebPart1"
   8:     Assembly="COB.SharePoint.WebPartAdderExtension, Version=1.0.0.0, Culture=neutral, PublicKeyToken=23afbf06fd91fa64"
   9:     Type="COB.SharePoint.WebPartAdderExtension.TeamsWebPart1.TeamsWebPart1"
  10:     IconUrl="_layouts/images/webpart.gif"
  11:     RibbonCommand=""
  12:     OnClientAdd=""
  13:     />
  14:   <WebPart
  15:   Title="My second web part for team sites - deployed via WebPartAdderExtension"
  16:   Category="COB"
  17:   Description="Another dummy web part for team sites"
  18:   ID="TeamsWebPart2"
  19:   Assembly="COB.SharePoint.WebPartAdderExtension, Version=1.0.0.0, Culture=neutral, PublicKeyToken=23afbf06fd91fa64"
  20:   Type="COB.SharePoint.WebPartAdderExtension.TeamsWebPart2.TeamsWebPart2"
  21:   IconUrl="_layouts/images/webpart.gif"
  22:   RibbonCommand=""
  23:   OnClientAdd=""
  24:     />
  25: </WebParts>

We then need some code to consume this. As discussed earlier, this must be a class derived from WebPartGallerySourceBase – since my implementation is ‘XML file per web application’, I created a base class which understands my XML schema and then a derived class per web app. It’s this latter class which knows where to find the XML file:

0: [Guid("98FF0ED8-6697-4E13-8BCC-161FEE3E4B0C")]
1: public class GlobalWebPartGallerySource_Teams : GlobalWebPartGalleryXmlSource

   2: {
   3:     public GlobalWebPartGallerySource_Teams(Page page)
   4:         : base(page)
   5:     {
   6:     }
   7:  
   8:     public override string XmlPath
   9:     {
  10:         get
  11:         {
  12:             string path = SPUtility.GetGenericSetupPath(@"template\layouts\COB.SharePoint.WebPartAdderExtension\GlobalWebParts\GlobalWebParts_Teams.xml");
  13:             return path;
  14:         }
  15:     }
  16: }

..and the base class simply reads from the chosen XML file and returns the collection (of GlobalWebPartGalleryItem instances in my case – the important thing is that it derives from WebPartGalleryItemBase):

   1: public class GlobalWebPartGalleryXmlSource : WebPartGallerySourceBase
   2: {
   3: public GlobalWebPartGalleryXmlSource(Page page)
   4:     : base(page)
   5: {
   6: }
   7:  
   8: public virtual string XmlPath
   9: {
  10:     get
  11:     {
  12:         return string.Empty;
  13:     }
  14: }
  15:  
  16: protected override WebPartGalleryItemBase[] GetItemsCore()
  17: {
  18:     List<WebPartGalleryItemBase> items = new List<WebPartGalleryItemBase>();
  19:     string path = XmlPath;
  20:  
  21:     if (File.Exists(path))
  22:     {
  23:         XElement root = XElement.Load(path);
  24:         var webParts = from wpDef in root.Elements("WebPart")
  25:                         select wpDef;
  26:  
  27:         foreach(XElement wp in webParts)
  28:         {
  29:             string title = wp.Attribute("Title").Value;
  30:             string category = wp.Attribute("Category").Value;
  31:             string description = wp.Attribute("Description").Value;
  32:             string iconUrl = wp.Attribute("IconUrl").Value;
  33:             string id = wp.Attribute("ID").Value;
  34:             string ribbonCommand = wp.Attribute("RibbonCommand").Value;
  35:             string onClientAdd = wp.Attribute("OnClientAdd").Value;
  36:             string assembly = wp.Attribute("Assembly").Value;
  37:             string typeName = wp.Attribute("Type").Value;
  38:  
  39:             items.Add(new GlobalWebPartGalleryItem(this, base.Page, title, category, description, iconUrl, id, assembly, typeName, onClientAdd, ribbonCommand));
  40:         }
  41:     }
  42:  
  43:     return items.ToArray();
  44: }

The key bit that links my GlobalWebPartGallerySource_Teams class above with my ‘teams’ web app, is a web app-scoped Feature, having the following elements:

   1: <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> 
   2:   <WebPartAdderExtension 
   3:       Assembly="$SharePoint.Project.AssemblyFullName$" 
   4:       Class="$SharePoint.Type.98ff0ed8-6697-4e13-8bcc-161fee3e4b0c.FullName$"/> 
   5: </Elements>

Note that I’m using the tokenization support to refer to my class using the GUID attribute it’s decorated with. NOTE – when using such tokens the GUID *must* be lowercase (I kid you not). This is just one of a couple of places in Feature/VS schema that I’m aware of which have this requirement.

Also, I’ll let you into a teensy secret, and say that my implementation has a couple of lines of reflection code in GlobalWebPartGalleryItem – this was necessary to carry the title through to the web part instance which was added to the page. Entirely optional, but since this is such an infrequent action in the grand scheme of web server traffic, it’s completely worth it IMHO for a consistent user experience.

In the end, the act of adding the web part to the page is just how the user expects. If you download my sample (link at the end of this article), here’s what it looks like here:

WebPartAdderExtension_TeamSitesWebPart

XML which is NOT needed (and must be deleted manually from the VS project)

It’s worth pointing here that you have to explicitly DELETE items from your project to use this approach. Remember that Visual Studio/CKS:Dev just know about the ‘regular’ way of developing web parts, and when you select ‘Add New Item> Visual Web Part’, items are added to your project which you no longer need – specifically this is the .webpart file and the elements.xml file which provisions this into the web part gallery. I’ve not tested, but I’d assume you get an error or duplicate if you forget to remove these. For clarity, it’s these:

Elements.xml (automatically added by Visual Studio) when web part added to project:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <!-- DO NOT USE - THIS XML IS NO LONGER NEEDED WHEN USING WEBPARTADDEREXTENSION -->
   3: <Elements xmlns="http://schemas.microsoft.com/sharepoint/" >
   4:   <Module Name="MySitesWebPart1" List="113" Url="_catalogs/wp">
   5:     <File Path="MySitesWebPart1\MySitesWebPart1.webpart" Url="MySitesWebPart1.webpart" Type="GhostableInLibrary" >
   6:       <Property Name="Group" Value="Custom" />
   7:     </File>
   8:   </Module>
   9: </Elements>

MySitesWebPart1.webpart:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <!-- DO NOT USE - THIS XML IS NO LONGER NEEDED WHEN USING WEBPARTADDEREXTENSION -->
   3: <webParts>
   4:   <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
   5:     <metaData>
   6:       <type name="COB.SharePoint.WebPartAdderExtension.WebParts.MySitesWebPart1.MySitesWebPart1, $SharePoint.Project.AssemblyFullName$" />
   7:       <importErrorMessage>$Resources:core,ImportErrorMessage;</importErrorMessage>
   8:     </metaData>
   9:     <data>
  10:       <properties>
  11:         <property name="Title" type="string">My web part title</property>
  12:         <property name="Description" type="string">My web part description</property>
  13:       </properties>
  14:     </data>
  15:   </webPart>
  16: </webParts>

And that’s it.

Summary

Although the old way of making custom web parts available has worked for years, that doesn’t mean it’s worked well! It turns out there’s an alternative which, in my opinion, has better characteristics – no more having to iterate each site collection if you ever need to make a change to the definition of a web part which is already out there. When I mentioned to our client that I’d used this new approach for our latest web part, the response was “Wow. I think we should go back and switch ALL our custom web parts over to this model.” I think Wictor gets the kudos for bringing it to folks attention though – I’m just providing some icing on the cake :)

Download link

Download my WebPartAdderExtension sample project

4 comments:

Brian said...

Yup, this is how they do it in Meeting Workspaces using the Microsoft.SharePoint.Meetings.MeetingsListGallerySource (internal/sealed).

Great write up around both the WHY and HOW for this!

Jason said...

Love this brother. Well played.

FatalFrenchy said...

Hey Chris,

How does this affect provisioning webparts in pages? I take it the AllUsersWebPart method is probably a no-no?

Thanks,

Justin

Chris O'Brien said...

Hey Justin,

That's a great question :) I haven't tried the combo of AllUsersWebPart and WebPartAdderExtension, but actually I wouldn't expect any issues. Consider that AllUsersWebPart takes a string ID in the declarative XML - both the .webpart file and WebPartAdderExtension require you to specify this.

Also, it's entirely possible to provision a ListViewWebPart onto a page using AllUsersWebPart, and those appear in the gallery courtesy of some Microsoft code which effectively uses the same approach (i.e. it's a WebPartGallerySource).

So I can't imagine there'd be any issues, but I'd love to hear from anyone who finds this isn't the case.

Cheers,

C.