Jeroen Derde's Blog

My Software Engineering Universe

Adding WebParts and a customised CQWP webpart to a page using powershell

clock November 11, 2011 11:45 by author Jeroen Derde
Update thanks to Japser Beerens:
if ($page.File.Level -eq [Microsoft.SharePoint.SPFileLevel]::Checkout)

 changed to

if ($page.File.CheckOutType.value__ -ne 2) #2 = SPCheckOutType.None

because first statement would fail if checked-out by someone else. 

This post will descibe what you need, and what to watch out for when you are planning to add webparts to a publishing page using powershell. Scroll down and skip the plumbing part for the good bits.
First of all: get PowerGUI (or PowerGui for visual studio using the extension gallery) this will make your life a whole lot easier.
Lets start with the information we have and create a batchfile. This is not required, but makes life a lot easier when you want to execute your powershell script multiple times, or have to deal with a Sys-Op that does not know any powershell.

The case: we would like to add webparts to multiple zones on publishing pages of a portal.
We have a site we want to add webparts to: http://contoso.com
In the webpart gallery we have a custom webpart (CQWP) with the title: "Related Documents"
This webpart needs to be added to the webpartzone with zoneid called "zone1" in all pages that have a certain pagelayout.
We create a batchfile and set all the parameters we need.
PS: The script is capable of putting multiple webparts into a single zone.
 
Batchfile
echo off
SET URL="http://contoso.com"
SET WEBPARTTOADD="Related Documents"
SET ZONEID="Zone1"

powershell.exe -command .\AddWebParts.ps1 '%URL%' '%WEBPARTTOADD%' '%ZONEID%'

pause
Now we can start on the AddWebParts.ps1 file.
Create the input params
Param($siteUrl,$WebPartsToAdd,$ZoneId);
Set the pagelayoutfilenames we want to add the webparts to, and we know the zoneid exists on.
This is required because the (very)LimitedWebpartManager does not allow us to check if a webpartzone exists in a page!
If the zone does not exist, SharePoint will just put the webpart in the first zone it can find.
  
$AllowedPageLayouts = "ArticleLeft.aspx","ArticleLeft.aspx";

Add the sharepoint powershell snapin

Add-PSSnapin Microsoft.SharePoint.PowerShell

With all the plumbing done we can start on actually doing something.
function InsertWebPartInWebPartZone($siteUrl, $WebPartsToAdd, $webPartZone)
{
	# Create a new instance of the site. 
	$site = Get-SPSite (New-Object System.Uri($siteUrl)); 

	# Iterate trough all the pages
	foreach($web in $site.AllWebs)
	{ 
		Write-Host "==============================="
		Write-Host "Currently parsing" $web.Title;
		Write-Host "==============================="

		#check if we are in a publising web
		if( ! [Microsoft.SharePoint.Publishing.PublishingWeb]::IsPublishingWeb($web) )
		{
			Write-Host "Web is not a publishing web, skipping"
			continue;
		}

		# Retrieve the pages library
		$pagesLibrary = $web.Lists["Pages"];
		$allowunsafeupdates = $web.AllowUnsafeUpdates
		$web.AllowUnsafeUpdates = $true

		# Iterate through all pages
		foreach($page in $pagesLibrary.Items)
		{
			#check for null
			if($page -eq $null)
			{
				continue;
			}

			#check if page is checked out already
			#if ($page.File.Level -eq [Microsoft.SharePoint.SPFileLevel]::Checkout)
			if ($page.File.CheckOutType.value__ -ne 2) #2 = SPCheckOutType.None
			{
				Write-Host $page.Url "is Checked-Out already. Skipping
			}
			else
			{

				#check if page is a publising page and if it is of allowed pagelayouts to change
				$pubPage = [Microsoft.SharePoint.Publishing.PublishingPage]::GetPublishingPage($page)
				if( ($pubpage -ne $null) -and ($AllowedPageLayouts -contains $pubPage.Layout.Name))
				{
					Write-Host "Page is of valid layout, processing page (" $page.Name ")"
				}
				else
				{
					Write-Host "Page is of invalid layout (" $pubPage.Layout.Name "). Skipping."
					continue;
				}

				Write-Host "starting page processing -----"
				# Checking out page
				$page.File.CheckOut()

				$index = 0;
				foreach($webPartToAdd in $WebPartsToAdd)
				{
					# Retrieve the webpart from the webpartgallery
					$webPart = GetWebPartFromGallery $site $webPartToAdd;
					if($webpart -eq $null)
					{
						Write-Host "Unable to retrieve $webPartToAdd to insert. Skipping.";
						continue;
					}

					# Check if the page already contains this webpart, if so, remove it
					RemoveWebPartFromPage $page $webPart.Title;

					# Add or re-add the webpart to the page in the specified zone
					AddWebPartToPage $page $webPart $webPartZone $index;

					# Update the index
					$index++;
				}

				# Build up the message
				$checkInMessage = "This page is checked in by the Add WebPart Script";

				# Check the page in and Publish if required
				$page.File.CheckIn($checkInMessage);
			
				if($page.ParentList.EnableMinorVersions -eq $true)
				{
					$Page.File.Publish("Published");
				}
				
				Write-Host "page done"
			}
		}

	$web.AllowUnsafeUpdates = $allowunsafeupdates;

	# Dispose of the web
	$web.Dispose();
	}
}
In the previous function we called several functions, in the next section we will outline these functions 
  • GetWebPartFromGallery
  • RemoveWebPartFromPage
  • AddWebPartToPage

Get WebPartFromGallery reads a webparts XML definition. It reads the definition from the webpartgallery and finds the webpart based on the webpart name.

function GetWebPartFromGallery($site, $webPartName)
{
	Write-Host "Getting " $webPartName " from gallery."
	
	# Get the rootweb (and get the webpart gallery)
	$web = $site.OpenWeb();
	$webPartGallery = $web.Lists["Web Part Gallery"]

	if($webPartGallery -eq $null)
	{
		Write-Host("Unable to retrieve Webpartgallery");
	}

	$webpart = $null;
	foreach($wp in $webPartGallery.Items)
	{
		#find the webpart we are looking for
		if($wp.Title -eq $webPartName)
		{
			$webpart = $wp;
			Write-Host "Webpart found in gallery"
			break;
		}
	}

	if($webpart -eq $null)
	{
		Write-Host("Unable to retrieve webpart: $webPartName");
	}

	return $webpart;
}
The next two functions do all the heavy lifting in this script (the good stuff).
The first function allows us to rerun the script multiple times. This is allows us to make changed to a webpart, upload it to the gallery and replace the existing webparts with new ones on all pages.
It searched the page for a webpart with a certain title. if the webpart is found on the page, the webpart will be removed. After that the second (add) function will be called.
 
The second function adds the webpart that was read from the gallery onto the page into the specific zone.
 
Both functions contain a section (needs refactoring :) ) thats sets the HTTPContext. This is the part of the functions that is very interesting, and basically the reason for writing this blogpost.
This code was added, because when we are instantiating webparts that contain URL's (like the CQWP contains an XslItemLink, XslMainLink or XslHeaderLink ) The properties of the webpart class are used to set the values in the webpart object.
These URL properties internally call makeserverrelative and that uses the HTTP context. However, because we are running inside of powershell, these is no HttpContext. This problem does not occur with the standard SharePoint webparts I tested, because the XML definition of these webparts does not contain any URL properties. But when trying to put the customised CQWP (related documents) on the page, I got an error. After a lot of debugging manually creating a HTTPContext was the solution because of the reason stated above.
I hope this information will save someone a lot of time.
#Remove webpart based on Webpart title
function RemoveWebPartFromPage($page, $webPartTitle)
{
#make sure the page is checked-out
if ($page.File.Level -ne [Microsoft.SharePoint.SPFileLevel]::Checkout)
{
Write-Host $page.Url " is not Checked-Out. Cannot remove webpart from the page.";
return $false;
} 

 
 
# set the current httpcontext, needed for contentquerywebparts webparts that use the 
 # xslitemlink, xslmainlink or xslheaderlink property
 # else makeserverrelative url will fail, because there no context
 if ($null -eq [System.Web.HttpContext]::Current)
 {
     $sw = New-Object System.IO.StringWriter
  $resp = New-Object System.Web.HttpResponse $sw
  $req = New-Object System.Web.HttpRequest "", $web.Url, ""
  $htc = New-Object System.Web.HttpContext $req, $resp
  #explicitly cast $web to spweb object else sharepoint will 
  #see it as a PSObject, and AddWebpart wil fail
  $htc.Items["HttpHandlerSPWeb"] = $web  -as [Microsoft.SharePoint.SPweb]
  [System.Web.HttpContext]::Current = $htc
 }

$webPartCollection = $page.Web.GetWebPartCollection($page.Url,[Microsoft.SharePoint.WebPartPages.Storage]::Shared);
$webPartStorageKey = $null;
foreach($wp in $webPartCollection)
{
$webpart =  $wp -as [Microsoft.SharePoint.WebPartPages.WebPart];
if($webpart.Title -eq $webPartTitle)
{
$webPartStorageKey = $webpart.StorageKey;
break; 
}
}

if($webPartStorageKey -eq $null)
{
return $false;
}

$webPartCollection.Delete($webPartStorageKey);
return $true;
}


# Adds the desired webpart to the page
function AddWebPartToPage($page, $webPartListItem, $webpartZoneId, $index)
{ 
Write-Host "Adding" $webPartListItem.Name " to " $page.Name;

if ($page.File.Level -ne [Microsoft.SharePoint.SPFileLevel]::Checkout)
{
Write-Host $page.Url "is not Checked-Out. Cannot add webpart to the page.";
return $false;
}

# Get the webpartmanager
$wpManager = $page.Web.GetLimitedWebPartManager($page.Url,[System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared);

# Get the webpart's xml
$errorMsg = "";
$xmlReader = New-Object System.Xml.XmlTextReader($webPartListItem.File.OpenBinaryStream());
$webPart = $wpManager.ImportWebPart($xmlReader, [ref]$errorMsg) -as [Microsoft.SharePoint.WebPartPages.WebPart];

try
{
# set the current httpcontext, needed for contentquerywebparts webparts that use the 
# xslitemlink, xslmainlink or xslheaderlink property
# else makeserverrelative url will fail, because there no context
if ($null -eq [System.Web.HttpContext]::Current)
{
    $sw = New-Object System.IO.StringWriter
$resp = New-Object System.Web.HttpResponse $sw
$req = New-Object System.Web.HttpRequest "", $web.Url, ""
$htc = New-Object System.Web.HttpContext $req, $resp
#explicitly cast $web to spweb object else sharepoint will 
#see it as a PSObject, and AddWebpart wil fail
$htc.Items["HttpHandlerSPWeb"] = $web  -as [Microsoft.SharePoint.SPweb]
[System.Web.HttpContext]::Current = $htc
}

# Add webpart to the page
$wpManager.AddWebPart($webPart, $webpartZoneId, $index); 
}
catch
{
Write-Host "An error occurred. Unable to add webpart " $webPartListItem.Title;
Write-Host $_.Exception.ToString();
}
[System.Web.HttpContext]::Current = $null
$wpManager.Dispose();


return $true;
}
         
I always start a Transcript before executing the script.
Start-Transcript

InsertWebPartInWebPartZone $siteUrl $WebPartsToAdd $ZoneId

Stop-Transcript




Microsoft Country Partner of the Year

clock April 21, 2011 19:58 by author Jeroen Derde

 At e-office we have been working hard at baking our Microsoft cake this year

Lets just say we are pretty serious about our baking skills.
To put the cherry on top we are submitting a great case to the anual Microsoft World Partner Conference in LA.




Month List

Calendar

<<  December 2014  >>
MoTuWeThFrSaSu
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar

Sign in