If you need to:
- Programmatically create a wiki page.
- Declaratively create a wiki page.
- Programmatically or Declaratively add or modify content on an existing wiki page.
- Programmatically or Declaratively deploy/add or remove/delete web parts from an existing (or new) wiki page
You should find this post useful…
I met these requirements in two simple steps:
Step 1 – Define a quick, re-usable schema that’ll allow someone to quickly declare what content and/or web parts are is to be added to which new or existing pages in what libraries
Step 2 – Develop a small provisioning class that i can throw the xml file at and have it provision as per the declarative instructions in a config file.
Config/Data File
My Config / data file looks something like:
So:
<Example> | Root Document Node |
<Library> | A Library node used to represent a wiki pages library. The URL of the library is represented by the ‘URL’ attribute. |
<Page> | A node to represent a single page in the library. The page name is represented by the ‘Name’ attribute. |
<Content> | The HTML content of the page encapsulated in <![CDATA[]]> |
<WebParts> | A node to represent the webparts to be added to the page |
<WebPart> | A node to represent a single web part. The Web Part ID (As declared in the content of the page) is declared using the ‘ID’ Attribute. The content of the web part is the exported web part xml |
The page content is pretty self explanatory from the screen shot and description above (simply throw html content inside a cdata section). Take note I’ve used the character pattern {<n>} to represent Web Part Place holders in the html. n in this case is any unique string/number to identify the web parts declared in the appropriate section.
Each <WebPart> node simply contains the xml contents of the exported web part again wrapped in a cdata section:
NB: Example below shows a content editor web part to be imported inline within a wiki page. I know (and I hope you know) that in the real world this doesn’t really make sense but will suffice for the purpose of this demonstration.
My second web part is a little more interesting, an XSLT List View Web Part. SharePoint does not allow you to export these web parts through the web UI. You’ll need to crack open Designer to get this:
After clicking the magic button, you’ll be prompted to supply a location to save your exported file… Once saved, crack taht open in Visual Studio/Your favourite editor and copy paste directly into a CData Section within the <WebPart> tag
Provisioning Class
My Provisioning class constructor looks something like:
public class PageDeployer
{
private XmlNode configNode;
public PageDeployer(XmlNode ConfigNode)
{
this.configNode = ConfigNode;
}
...
}
Taking note that upon instantiation, it’s expecting a <Library> Node…
A Deploy method within the class accepts a single SPWeb Object and is responsible for doing the work:
public void Deploy(SPWeb web)
{
SPList list = this.GetListFromWebRelativeURL(web,this.configNode.Attributes["URL"].Value);
foreach (XmlNode pageNode in this.configNode.SelectNodes("Page"))
{
//get the page name from the xml attribute:
string pageFileName = pageNode.Attributes["Name"].Value;
//build the file URL:
string fileURL = string.Format("{0}/{1}", list.RootFolder.ServerRelativeUrl, pageFileName);
bool fileExisted = true;
SPFile file = web.GetFile(fileURL);
if (!file.Exists)
{
//File does not exist, we need to create it:
file = SPUtility.CreateNewWikiPage(list, fileURL);
fileExisted = false;
}
//get the content as defined in the xml provisioning file
string content = ((XmlCDataSection)pageNode.SelectSingleNode("Content").FirstChild).Value.Trim();
//grab reference to the web part manager for the page:
using (SPLimitedWebPartManager splwm =
file.GetLimitedWebPartManager(System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared))
{
if (fileExisted)
{
while (splwm.WebParts.Count > 0)
{
splwm.DeleteWebPart(splwm.WebParts[0]);
}
}
//iterate through each web part node:
foreach (XmlNode webPartNode in pageNode.SelectNodes("WebParts/WebPart"))
{
//dynamically add the web part and modify the contents
content = InsertWebPart(webPartNode, content, splwm);
}
}
//grab reference to the item:
SPListItem item = file.GetListItem(new string[] { "WikiField" });
//update the 'WikiField' field:
item["WikiField"] = content;
//commit the changes:
item.Update();
}
}
Nothing too special there – iterate through the page / web part nodes reading the properties of the config/xml file… The magic happens in the ‘InsertWebPart’ method which is responsible for:
- Creating WebPart Objects by loading the web part xml
- Adding web part objects to the web part manager
- Dynamically inserting a set of divs that the sharepoint rendering engine will later use to dynamically insert the web parts added to the SPLimitedWebPartManager
What is returned after doing all that is a single string representing the content of the wiki page. To use this, simply set the field ‘WikiField’ to this value (demonstrated in last couple of lines in the ‘Deploy’ method outlined above).
private string InsertWebPart(XmlNode webPartNode, string wikiContent, SPLimitedWebPartManager WebPartManager)
{
//get the provisioning file web part ID:
string webPartID = webPartNode.Attributes["ID"].Value;
//ensure that there's a place holder in the content for it.
if (wikiContent.Contains(string.Format("{{<{0}>}}", webPartID)))
{
//get the web part xml:
string webPartContent = ((XmlCDataSection)webPartNode.FirstChild).Value.Replace("\n",string.Empty).Replace("\r",string.Empty).Trim();
//create the web part using the web part manager
XmlReader xreader = XmlReader.Create(new System.IO.StringReader(webPartContent.Trim()));
string WebPartImportErrorMsg = string.Empty;
System.Web.UI.WebControls.WebParts.WebPart webpart = WebPartManager.ImportWebPart(xreader, out WebPartImportErrorMsg);
//create a new web part IDs:
Guid wpID = Guid.NewGuid();
string containingDivID = wpID.ToString();
string webPartObjID = this.StorageKeyToID(wpID);
//set web part object with appropriate ID
webpart.ID = webPartObjID;
//add the web part
WebPartManager.AddWebPart(webpart, "wpz", 0);
//build the wiki content place holder
string webPartPlaceHolder = this.GetWikiWebPartContainer(containingDivID);
//replace the provisioning place holder with the one required by the wiki page:
wikiContent = wikiContent.Replace(string.Format("{{<{0}>}}", webPartID), webPartPlaceHolder);
}
return wikiContent;
}
private string GetWikiWebPartContainer(string containingDivID)
{
StringBuilder sb = new StringBuilder("<div class=\"ms-rtestate-read ms-rte-wpbox\"><div class=\"ms-rtestate-read ");
sb.Append(containingDivID);
sb.Append("\" id=\"div_");
sb.Append(containingDivID);
sb.Append("\"></div>\n");
sb.Append(" <div class=\"ms-rtestate-read\" id=\"vid_");
sb.Append(containingDivID);
sb.Append("\" style=\"display:none\"></div>\n");
sb.Append("</div>\n");
string webPartPlaceHolder = sb.ToString();
return webPartPlaceHolder;
}
private string StorageKeyToID(Guid storageKey)
{
if (!(Guid.Empty == storageKey))
{
return ("g_" + storageKey.ToString().Replace('-', '_'));
}
return string.Empty;
}
So in summary,
I use a small console app like this:
static void Main(string[] args)
{
using (SPSite site = new SPSite("http://iitb01/sites/test1"))
{
using (SPWeb web = site.OpenWeb())
{
string sampleDataFilePath = @"C:\SampleData\Pages.xml";
XmlDocument configDoc = new XmlDocument();
configDoc.Load(sampleDataFilePath);
PageDeployer deployer =
new PageDeployer(configDoc.SelectSingleNode("//Library"));
deployer.Deploy(web);
}
}
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("All Done!");
Console.ReadKey();
}
To throw a config file like this:
<?xml version="1.0" encoding="utf-8" ?>
<Example>
<Library URL="SitePages">
<Page Name="WP3.aspx">
<Content>
<![CDATA[
<table>
<tr>
<td width="66%" valign="top">
<div style="font-size:2em;">Hello World!!</div>
<div style="font-style:italic; padding-left:8px; padding-top:4px">
gone walkabout bloody as dry as a kindie. We're going cark it piece of piss you little ripper bludger. You little ripper banana bender shazza got us some bonza. She'll be right give it a burl with as busy as a budgie smugglers. It'll be bush oyster when shazza got us some bail up. As busy as a bonza mate trent from punchy larrikin. Shazza got us some battler piece of piss lets throw a butcher. Shazza got us some rage on how lets get some doovalacky. Grab us a slaps when stands out like a bogged. As stands out like dead horse piece of piss built like a digger.
</div>
<br/>
{<0>}
<div style="font-size:1.1em;padding-top:12px;padding-bottom:12px;">And Here is an example of a list view web part</div>
{<1>}
</td>
<td>
<img alt="People collaborating" src="/_layouts/images/homepageSamplePhoto.jpg" style="margin:5px" />
</td>
</tr>
</table>
]]>
</Content>
<WebParts>
<WebPart ID="0">
<![CDATA[
<?xml version="1.0" encoding="utf-8" ?>
<WebPart xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/WebPart/v2">
<Assembly>Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
<TypeName>Microsoft.SharePoint.WebPartPages.ContentEditorWebPart</TypeName>
<Title>Content Editor</Title>
<FrameType>Default</FrameType>
<Description>Allows authors to enter rich text content.</Description>
<IsIncluded>true</IsIncluded>
<PartOrder>0</PartOrder>
<FrameState>Normal</FrameState>
<Height />
<Width />
<AllowRemove>true</AllowRemove>
<AllowZoneChange>true</AllowZoneChange>
<AllowMinimize>true</AllowMinimize>
<AllowConnect>true</AllowConnect>
<AllowEdit>true</AllowEdit>
<AllowHide>true</AllowHide>
<IsVisible>true</IsVisible>
<DetailLink />
<HelpLink />
<HelpMode>Modeless</HelpMode>
<Dir>Default</Dir>
<PartImageSmall />
<MissingAssembly>Cannot import this Web Part.</MissingAssembly>
<PartImageLarge>/_layouts/images/mscontl.gif</PartImageLarge>
<IsIncludedFilter />
<ExportControlledProperties>true</ExportControlledProperties>
<ContentLink xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
<Content xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor"><span class="ms-rteThemeForeColor-8-3 ms-rteStyle-Comment">​CONTENT EDITOR WEB
PART</span></Content>
<PartStorage xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
</WebPart>
]]>
</WebPart>
<WebPart ID="1">
<![CDATA[
<?xml version="1.0" encoding="utf-8" ?>
<webParts>
<webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
<metaData>
<type name="Microsoft.SharePoint.WebPartPages.XsltListViewWebPart, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
<importErrorMessage>Cannot import this Web Part.</importErrorMessage>
</metaData>
<data>
<properties>
<property name="InitialAsyncDataFetch" type="bool">False</property>
<property name="ChromeType" type="chrometype">Default</property>
<property name="Title" type="string" />
<property name="Height" type="string" />
<property name="CacheXslStorage" type="bool">True</property>
<property name="ListDisplayName" type="string" />
<property name="AllowZoneChange" type="bool">True</property>
<property name="AllowEdit" type="bool">True</property>
<property name="XmlDefinitionLink" type="string" />
<property name="DataFields" type="string" />
<property name="Hidden" type="bool">False</property>
<property name="ListName" type="string" null="true" />
<property name="NoDefaultStyle" type="string" null="true" />
<property name="AutoRefresh" type="bool">False</property>
<property name="ViewFlag" type="string">8388621</property>
<property name="Direction" type="direction">NotSet</property>
<property name="AutoRefreshInterval" type="int">60</property>
<property name="AllowConnect" type="bool">True</property>
<property name="Description" type="string" />
<property name="AllowClose" type="bool">True</property>
<property name="ShowWithSampleData" type="bool">False</property>
<property name="ParameterBindings" type="string">
<ParameterBinding Name="dvt_sortdir" Location="Postback;Connection"/>
<ParameterBinding Name="dvt_sortfield" Location="Postback;Connection"/>
<ParameterBinding Name="dvt_startposition" Location="Postback" DefaultValue=""/>
<ParameterBinding Name="dvt_firstrow" Location="Postback;Connection"/>
<ParameterBinding Name="OpenMenuKeyAccessible" Location="Resource(wss,OpenMenuKeyAccessible)" />
<ParameterBinding Name="open_menu" Location="Resource(wss,open_menu)" />
<ParameterBinding Name="select_deselect_all" Location="Resource(wss,select_deselect_all)" />
<ParameterBinding Name="idPresEnabled" Location="Resource(wss,idPresEnabled)" />
<ParameterBinding Name="NoAnnouncements" Location="Resource(wss,noXinviewofY_LIST)" />
<ParameterBinding Name="NoAnnouncementsHowTo" Location="Resource(wss,noXinviewofY_DEFAULT)" />
</property>
<property name="Xsl" type="string" null="true" />
<property name="CacheXslTimeOut" type="int">86400</property>
<property name="WebId" type="System.Guid, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">00000000-0000-0000-0000-000000000000</property>
<property name="ListUrl" type="string">Lists/IITBComponents</property>
<property name="DataSourceID" type="string" />
<property name="FireInitialRow" type="bool">True</property>
<property name="ManualRefresh" type="bool">False</property>
<property name="ViewFlags" type="Microsoft.SharePoint.SPViewFlags, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">Html, TabularView, Hidden, Mobile</property>
<property name="ChromeState" type="chromestate">Normal</property>
<property name="AllowHide" type="bool">True</property>
<property name="PageSize" type="int">-1</property>
<property name="SampleData" type="string" null="true" />
<property name="BaseXsltHashKey" type="string" null="true" />
<property name="AsyncRefresh" type="bool">False</property>
<property name="HelpMode" type="helpmode">Modeless</property>
<property name="ListId" type="System.Guid, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">00000000-0000-0000-0000-000000000000</property>
<property name="DataSourceMode" type="Microsoft.SharePoint.WebControls.SPDataSourceMode, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">List</property>
<property name="AllowMinimize" type="bool">True</property>
<property name="TitleUrl" type="string">/sites/test1/Lists/IITBComponents</property>
<property name="CatalogIconImageUrl" type="string">/_layouts/images/itgen.png</property>
<property name="DataSourcesString" type="string" />
<property name="GhostedXslLink" type="string">main.xsl</property>
<property name="PageType" type="Microsoft.SharePoint.PAGETYPE, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">PAGE_NORMALVIEW</property>
<property name="DisplayName" type="string" />
<property name="UseSQLDataSourcePaging" type="bool">True</property>
<property name="Width" type="string" />
<property name="ExportMode" type="exportmode">All</property>
<property name="XslLink" type="string" null="true" />
<property name="ViewContentTypeId" type="string">0x</property>
<property name="HelpUrl" type="string" />
<property name="XmlDefinition" type="string">
<View Name="{108EA0FF-5634-4176-A1D7-DDF8A1A2421F}" MobileView="TRUE" Type="HTML" Hidden="TRUE" DisplayName="" Url="/sites/test1/SitePages/WP2.aspx" Level="1" BaseViewID="1" ContentTypeID="0x" ImageUrl="/_layouts/images/generic.png">
<Query>
<OrderBy>
<FieldRef Name="ID"/>
</OrderBy>
</Query>
<ViewFields>
<FieldRef Name="Attachments"/>
<FieldRef Name="LinkTitle"/>
</ViewFields>
<RowLimit Paged="TRUE">30</RowLimit>
<Toolbar Type="Freeform"/>
</View>
</property>
<property name="Default" type="string" />
<property name="TitleIconImageUrl" type="string" />
<property name="MissingAssembly" type="string">Cannot import this Web Part.</property>
<property name="SelectParameters" type="string" />
</properties>
</data>
</webPart>
</webParts>
]]>
</WebPart>
</WebParts>
</Page>
</Library>
</Example>
At a provisioning class that looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebPartPages;
namespace MyriadTech.SharePoint2010.WikiPageDeployer
{
public class PageDeployer
{
private XmlNode configNode;
public PageDeployer(XmlNode ConfigNode)
{
this.configNode = ConfigNode;
}
public void Deploy(SPWeb web)
{
SPList list = this.GetListFromWebRelativeURL(web,this.configNode.Attributes["URL"].Value);
foreach (XmlNode pageNode in this.configNode.SelectNodes("Page"))
{
//get the page name from the xml attribute:
string pageFileName = pageNode.Attributes["Name"].Value;
//build the file URL:
string fileURL = string.Format("{0}/{1}", list.RootFolder.ServerRelativeUrl, pageFileName);
bool fileExisted = true;
SPFile file = web.GetFile(fileURL);
if (!file.Exists)
{
//File does not exist, we need to create it:
file = SPUtility.CreateNewWikiPage(list, fileURL);
fileExisted = false;
}
//get the content as defined in the xml provisioning file
string content = ((XmlCDataSection)pageNode.SelectSingleNode("Content").FirstChild).Value.Trim();
//grab reference to the web part manager for the page:
using (SPLimitedWebPartManager splwm = file.GetLimitedWebPartManager(System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared))
{
if (fileExisted)
{
while (splwm.WebParts.Count > 0)
{
splwm.DeleteWebPart(splwm.WebParts[0]);
}
}
//iterate through each web part node:
foreach (XmlNode webPartNode in pageNode.SelectNodes("WebParts/WebPart"))
{
//dynamically add the web part and modify the contents
content = InsertWebPart(webPartNode, content, splwm);
}
}
//grab reference to the item:
SPListItem item = file.GetListItem(new string[] { "WikiField" });
//update the 'WikiField' field:
item["WikiField"] = content;
//commit the changes:
item.Update();
}
}
private string InsertWebPart(XmlNode webPartNode, string wikiContent, SPLimitedWebPartManager WebPartManager)
{
//get the provisioning file web part ID:
string webPartID = webPartNode.Attributes["ID"].Value;
//ensure that there's a place holder in the content for it.
if (wikiContent.Contains(string.Format("{{<{0}>}}", webPartID)))
{
//get the web part xml:
string webPartContent = ((XmlCDataSection)webPartNode.FirstChild).Value.Replace("\n",string.Empty).Replace("\r",string.Empty).Trim();
//create the web part using the web part manager
XmlReader xreader = XmlReader.Create(new System.IO.StringReader(webPartContent.Trim()));
string WebPartImportErrorMsg = string.Empty;
System.Web.UI.WebControls.WebParts.WebPart webpart = WebPartManager.ImportWebPart(xreader, out WebPartImportErrorMsg);
//create a new web part IDs:
Guid wpID = Guid.NewGuid();
string containingDivID = wpID.ToString();
string webPartObjID = this.StorageKeyToID(wpID);
//set web part object with appropriate ID
webpart.ID = webPartObjID;
//add the web part
WebPartManager.AddWebPart(webpart, "wpz", 0);
//build the wiki content place holder
string webPartPlaceHolder = this.GetWikiWebPartContainer(containingDivID);
//replace the provisioning place holder with the one required by the wiki page:
wikiContent = wikiContent.Replace(string.Format("{{<{0}>}}", webPartID), webPartPlaceHolder);
}
return wikiContent;
}
private string GetWikiWebPartContainer(string containingDivID)
{
StringBuilder sb = new StringBuilder("<div class=\"ms-rtestate-read ms-rte-wpbox\"><div class=\"ms-rtestate-read ");
sb.Append(containingDivID);
sb.Append("\" id=\"div_");
sb.Append(containingDivID);
sb.Append("\"></div>\n");
sb.Append(" <div class=\"ms-rtestate-read\" id=\"vid_");
sb.Append(containingDivID);
sb.Append("\" style=\"display:none\"></div>\n");
sb.Append("</div>\n");
string webPartPlaceHolder = sb.ToString();
return webPartPlaceHolder;
}
private string StorageKeyToID(Guid storageKey)
{
if (!(Guid.Empty == storageKey))
{
return ("g_" + storageKey.ToString().Replace('-', '_'));
}
return string.Empty;
}
private SPList GetListFromWebRelativeURL(SPWeb web, string WebRelativeListURL)
{
return web.GetList(string.Format("{0}/{1}", web.ServerRelativeUrl, WebRelativeListURL));
}
}
}
And i get: