Adding Web Parts to SharePoint 2010 Wiki Pages using the Client Object Model

Update 2010-10-24: Added info about the layoutsData span and changed wording mostly to clarify which client OM I was using and why.

When trying to add a Web Part to a SharePoint 2010 team site using the Client Object Model (client OM) I found lots of posts. Unfortunately they either didn’t use the client OM or the team site wasn’t based on wiki pages. Actually I still haven’t found a single post covering this scenario. So here’s how to do it.

The Big Picture

If you add a Web Part to a Web Part page you simply tell the Web Part manager in which zone to put it. But if your page is a wiki page you also have to mark the place in HTML where the Web Part should show up. And as you probably know SharePoint 2010 team sites are based on wiki pages by default.

This was for setting up automated tests on a remote machine so I’m using the managed client OM. After acquiring the client context the next step is to get a reference that lets you access both the Web Part manager and the wiki content. The wiki content is stored in an additional form field named WikiField and you need the page as a ListItem reference to access it. A reference to the page as a File object will not work.

A small twist: if you have the .wsp file but not the .webpart file you’ll have to extract it manually as there is no way to get at the contents of a .wsp file using managed code.

Step 1: Adding the Web Part to the Web Part Zone

Once you have got the .wsp file you can use it’s content to have the Web Part manager insert the Web Part into the wiki page’s one and only Web Part zone. This zone is hidden and shows up in the HTML with an ID like ctl00_panelZone.

Some details on using the Web Part manager:

  • The WebPartDefinition’s Id property is read-only and you have to call ImportWebPart() to have a GUID created for you. This is different from SPWebPart where you can specify a value.
  • AddWebPart() adds the Web Part to the wiki page’s single Web Part zone called “wpz”–you can check that name by spying into Microsoft.SharePoint.WebPartPages.WikiEditPage’s InsertWebPartIntoWikiPage() with Reflector.
  • There’s no way to check whether a page already contains a specific type of Web Part using the client OM. It reports all Web Parts as being of type Microsoft.SharePoint.Client.WebParts.WebPart.

Step 2: Adding a Reference to the Web Part into the HTML

The final step is to add a reference to the Web Part into the HTML–as you can imagine this defines where the Web Part will actually show up. The reference is created by three div elements like below referencing the Web Part’s Id. It’s based on a GUID and in this case is g_2252e9e2_86d2_4c6a_9c60_6de78cd15c84.

<!-- Web Part's divs -->
<div class="ms-rtestate-read ms-rte-wpbox">
 <div 
  class="ms-rtestate-notify ms-rtegenerate-notify ms-rtestate-read 2252e9e2-86d2-4c6a-9c60-6de78cd15c84" 
  id="div_2252e9e2-86d2-4c6a-9c60-6de78cd15c84" 
 />
 <div 
  style='display:none' 
  id='vid_2252e9e2-86d2-4c6a-9c60-6de78cd15c84'
 />
</div>

The divs described above go into the wiki field’s HTML. If you are using the default page layout the HTML consists of a div containing a two column table like this:

<!-- Wiki field's contents -->
<div class="ExternalClass539BB6C7BA304446AA96FF3B95F23388">
 <table id="layoutsTable" style="width:100%">
  <tbody>
   <tr style="vertical-align:top">
    <td style="width:66.6%">
     <!-- Contents of left column -->
    </td>
    <td style="width:33.3%">
     <!-- Contents of right column -->
     <div class="ms-rte-layoutszone-outer" style="width:100%">
      <div class="ms-rte-layoutszone-inner" style="min-height:60px;word-wrap:break-word">
       <!-- Code adds Web Part at end of div's elements -->
      </div>
     </div>
    </td>
   </tr>
  <tbody>
 </table>
 <span id="layoutsData" style="display:none">false,false,1</span>
</div>

Some additional notes:

  • If you create the inner divs as empty elements the “/” will be missing from the HTML–so create them with opening and closing tags instead.
  • PreserveWhitespace isn’t really required but helps the readability of the generated HTML
  • Instead of XmlDocument you could use the more fashionable XElement.
  • If you mess up the HTML it might be less work to rebuild the page than to clean it up.
  • The span in line 20 stores the layout–in this case: no header, no footer,1 column. While it can represent the built-in layouts the format seems a bit limiting for additional layouts.

All Together Now …

And here’s the code to make it all happen–just remember to change strings like “Site Pages” to values for your locale:

// Needs references to SharePoint DLLs in
// C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI\
using System;
using System.Diagnostics;
using System.Linq;
using System.Xml;
using Microsoft.SharePoint.Client;
using Microsoft.SharePoint.Client.WebParts;
using io = System.IO;

namespace TestProject1 {
 class WebPartMgmtHelper {
  public static void AddWebPart(string fqWebPartFileName, string hostName) {
   using (ClientContext clientContext = new ClientContext(hostName)) {
    try {
     // Get references to WebPartManager and HTML
     List list = clientContext.Web.Lists.GetByTitle("Site Pages");
     CamlQuery camlQuery = new CamlQuery();
     ListItemCollection listItems = list.GetItems(camlQuery);
     clientContext.Load(listItems,
      items => items.Include(
       item => item.DisplayName, item => item["WikiField"]).Where(
        item => item.DisplayName == "Home"));
     clientContext.ExecuteQuery();
     LimitedWebPartManager wpm =
      listItems[0].File.GetLimitedWebPartManager(PersonalizationScope.Shared);
     // Add WebPart to zone
     string webPartFileContents = io.File.ReadAllText(fqWebPartFileName);
     WebPartDefinition wpd = wpm.ImportWebPart(webPartFileContents);
     WebPartDefinition wpdNew = wpm.AddWebPart(wpd.WebPart, "wpz", 0);
     clientContext.Load(wpdNew);
     clientContext.ExecuteQuery();
     Debug.WriteLine("AddWebPart(): wpdNew.Id = {0}", new object[] { wpdNew.Id });
     // Create reference to WebPart in HTML
     string wikiField = listItems[0]["WikiField"] as string;
     XmlDocument xd = new XmlDocument();
     xd.PreserveWhitespace = true;
     xd.LoadXml(wikiField);
     XmlElement layoutsTable = xd.SelectSingleNode("div/table") as XmlElement;
     XmlElement layoutsZoneInner 
      = layoutsTable.SelectSingleNode("tbody/tr/td[2]/div/div") as XmlElement;
     // - wpBoxDiv
     XmlElement wpBoxDiv = xd.CreateElement("div");
     layoutsZoneInner.AppendChild(wpBoxDiv);
     XmlAttribute attribute = xd.CreateAttribute("class");
     wpBoxDiv.Attributes.Append(attribute);
     attribute.Value = "ms-rtestate-read ms-rte-wpbox";
     attribute = xd.CreateAttribute("contentEditable");
     wpBoxDiv.Attributes.Append(attribute);
     attribute.Value = "false";
     // - div1
     XmlElement div1 = xd.CreateElement("div");
     wpBoxDiv.AppendChild(div1);
     div1.IsEmpty = false;
     attribute = xd.CreateAttribute("class");
     div1.Attributes.Append(attribute);
     attribute.Value = "ms-rtestate-read " + wpdNew.Id.ToString("D");
     attribute = xd.CreateAttribute("id");
     div1.Attributes.Append(attribute);
     attribute.Value = "div_" + wpdNew.Id.ToString("D");
     // - div2
     XmlElement div2 = xd.CreateElement("div");
     wpBoxDiv.AppendChild(div2);
     div2.IsEmpty = false;
     attribute = xd.CreateAttribute("style");
     div2.Attributes.Append(attribute);
     attribute.Value = "display:none";
     attribute = xd.CreateAttribute("id");
     div2.Attributes.Append(attribute);
     attribute.Value = "vid_" + wpdNew.Id.ToString("D");
     // Update
     listItems[0]["WikiField"] = xd.OuterXml;
     listItems[0].Update();
     clientContext.ExecuteQuery();
     Debug.WriteLine("AddWebPart(): OK");
    }
    catch (Exception x) {
     Debug.WriteLine("---EXCEPTION---AddWebPart(): {0}", new object[] { x.Message });
    }
   } // using (ClientContext)
  } // AddWebPart()
 } // class WebPartMgmtHelper
} // namespace TestProject1



Advertisements
This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

10 Responses to Adding Web Parts to SharePoint 2010 Wiki Pages using the Client Object Model

  1. Pingback: Getting Rid of Web Part’s Remains in Sharepoint 2010 | Dieter Bremes's Blog

  2. Nick Larter says:

    Hi Dieter, thanks for such a compelling article — I too have struggled to find any info on how to programmatically customise wiki pages. I still need some help however…

    I have added a custom button to the SharePoint Ribbon which launches a simple modal dialog. No problems so far. When the user hits OK and closes the window, I am trying to add a web part inside the RTE (this appears to be slightly different to your example, where you add the web part to the list item’s HTML field and then update/save it). I know the scenario I’m trying to achieve is possible because the SharePoint Ribbon already exposes this very same functionality! (if you edit the page, then click Insert -> Web Part -> Contact Details). This puts a Contact Details web part inside the RTE at the current cursor location with the exact same HTML as you have outlined in your example.

    Could you please shed any light on how I might go about doing this? I can’t find any documentation on how to programmatically insert a web part into the RTE — nor can I find the code in the SharePoint assemblies which does this.

    Many many thanks!

    • dbremes says:

      Hi Nick,

      to add a web part while the user is editing your best bet would be to figure out how the ribbon buttons do it–I never tried. This post should get you started: Sharepoint 2010 Ribbon Control and Rich Text Editor. You could also have a look at wpadder.debug.js in C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS.

      Thanks for your interest in my blog
      Dieter

      • Nick Larter says:

        Coincidentally, the article you mentioned is also the only one I could find that features any client side script for manipulating text in the RTE editor pane.

        “wpadder.debug.js” has already proven useful though… I’m able to add OOTB web parts directly into the editor by simply calling some of the functions in it. For those playing at home: the following JavaScript function adds the web part reference divs to the HTML and returns you the ID of the new part:

        var webPartId = _WPAdder.prototype._createWebpartPlaceholderInRte()

        From here, the script issues a simple postback passing in the web part zone ID and index (“wpz” and 0, as metioned in the post) and adds the web part to the page content. In my case, I need to set some properties on the web part before it gets added to the page, so I will try overriding this postback code. I’ll add a working code sample below as soon as I’ve got it done for anybody experiencing the same pains as me… Thanks again.

  3. Cyril Berber says:

    Hi, I need the same functionality (custom button in the ribbon to add a web part in RTE in edit mode), but I can’t view the Web Part in RTE… how do you do this ?

    • dbremes says:

      Sounds like Nick’s scenario. I’m afraid I can’t add anything to what he and I wrote already. I never tried adding Web Parts using the JavaScript client OM.

  4. Nick Larter says:

    I assume you already know how to add your custom button to the ribbon? If not, or you don’t understand any of the terms I’ve used below, refer to this post.

    Once you have your ribbon button defined as a CustomAction your Elements.xml, you only need to call one or two of the built-in SharePoint JavaScript functions to get your web part added to the RTE. Here’s how…

    1. Inside your CommandUIHandler, you will first need to ensure that the window.WPAdder object has been instantiated (this is the client-side object that SharePoint uses to add web parts to the page, so it does all the work for you).

    2. You need to locate the source web part you want to add to the page in the gallery. I have done this by iterating through each category and item in the list and then comparing the Title values. You could use the ID or anything else to match on.


    var sourceWebPart = GetSourceWebPart("My Web Part");

    function GetSourceWebPart(title) {
    for (var c = 0; c < window.WPAdder._cats.length; c++) {
    var webParts = window.WPAdder._cats[c].items;

    for (var i = 0; i < webParts.length; i++) {
    if (webParts[i].title.toLowerCase() == title.toLowerCase())
    return webParts[i];
    }
    }
    }

    3. You simply call the following function which internally, will “clone” your source web part, add it to the page, add the mark-up to the RTE, and then refresh the RTE. It is this “refresh” that is probably preventing you from seeing your web part in the editor.


    window.WPAdder._addItemToPage(sourceWebPart, '0', 0);

    The other parameters I have passed in to this function are the web part zone ID and index. These are both zero, and are always the same on publishing pages.

    Done!

  5. Teres says:

    There is also a little bit less complicated way of doing the same http://sharepointstruggle.blogspot.com/2011/10/programmatically-provisioning-wiki.html

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s