I've been working on a new internal portal for my employer using SharePoint 2010. As the project progressed, we experimented with several custom page layouts and, at length, settled on our two favorites.

With the decision final, we reassigned all the pages to the authorized page layouts and proceeded to delete the extras... all except one.

This page layout didn't want to get deleted. I was persistent, but the file was more so.

 

  • Deleting the file in SharePoint Designer yielded the error "Server error: This item cannot be deleted because it is still referenced by other pages." Mind you, it wasn't, but SharePoint thought it was.
  • Several online sources indicated that moving it to a sub-folder should allow deletion of the folder. However, in my case, doing so in SharePoint Designer yielded the error "Server error: This folder cannot be deleted because it contains the following page layout that is still in use: LayoutPage01.aspx."
  • I could open the master page gallery in Windows Explorer, attempt to delete the file, or move it to a folder and delete the folder, and yet the file remained.
  • I fired up PowerShell and attempted to delete the file with code, and received the same error.
At this point I didn't quite know what to do. As a matter of fact, poking around the code with PowerShell didn't help me root out the source of the error; or, at least, not at first.

I was working on another task--one related to event receivers--when I noticed the following event receiver attached to the master page gallery:
Type                        : ItemDeleting
SequenceNumber              : 1000
Assembly                    : Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c
Class                       : Microsoft.SharePoint.Publishing.Internal.BlockDeletionOfInUseItemsEventReceiver
"Well, well," I thought. This must be the source of the error.

Upon inspection, I learned about the BackwardLinks property of the SPFile class. This collection provides information as to *which* files still lay claim on the page layout.

I took a look at the BackwardLinks collection on my particular page layout in PowerShell, and discovered four backward links, all from files that didn't exist anymore!

Well, enough story. Here is the PowerShell I used to resolve the problem. It will probably break your system and destroy all that you love... Don't say I didn't warn you, if you try to use it. The steps are simple:
  1. Find the layout in the gallery.
  2. Check to see if any of the BackwardLinks are to files that really exist.
  3. If non of the files exist, remove the event receiver that prevents deletion, delete the file, and then re-register the event receiver.
Happy Coding!

function Remove-PageLayout($siteUrl, $layoutName) {
  $site       = Get-SPSite $siteUrl
  $web        = $site.RootWeb
  $gallery    = $web.lists["Master Page Gallery"]
  $layoutItem = $gallery.Items |? { $_.Name -eq $layoutName }
  if(!$layoutItem) { throw (new-object IO.FileNotFoundException) }

  $layoutFile = $layoutItem.File

  $layoutFile.BackwardLinks |% {
    if(Test-Page $_.Url) {
      throw "Cannot delete page because it is referenced by $($_.Url)."
    }
  }

  $receiverType     = [Microsoft.SharePoint.Publishing.Internal.BlockDeletionOfInUseItemsEventReceiver]
  $unregisterMethod = $receiverType.GetMethod("Unregister", [reflection.bindingflags]"Static,NonPublic")
  $registerMethod   = $receiverType.GetMethod("RegisterIfNeeded", [reflection.bindingflags]"Static,NonPublic")

  $unregisterMethod.Invoke($null, $gallery)
  $layoutItem.Delete()
  $registerMethod.Invoke($null, $gallery)

  $site.Dispose();
}

function Test-Page($url) {

  $site = $web = $null

  trap [exception] {
    if($web) { $web.dispose() }
    if($site) { $site.dispose() }
    return $false
  }

  $site = new-object Microsoft.SharePoint.SPSite($url)
  $web = $site.openweb()

  $file = $web.GetFile($url)
  $result = $file.exists

  $web.dispose()
  $site.dispose()
  return $result
}

 
Categories: PowerShell | SharePoint

Background

I came across an interesting question recently. How does one execute a PowerShell script in a Visual Studio project's pre- or post-build event, and get precise error message feedback when errors occur.

Part of the problem is that errors seem to get "swallowed" in the PowerShell script. Output can be directed to the console as the post-build event occurs, but nothing gets sent to the error window, and the build is not interrupted on error.

My initial inclination was to simply use PowerShell error trapping and exit() codes in the PowerShell script to classify errors, but that hardly pleases the imagination. At least it provides build termination if you call exit() with any non-zero value, but it lacks precision.

When compilation errors occur in C# code, Visual Studio provides line numbers and column positions of the error, with the handly double-click jump-to-error goodness that makes tracking down the error at least a little less tedious. Using exit() codes does not provide the same quality of service for PowerShell scripts.

So, in an attempt to find the grail, I developed a small MSBuild task for executing PowerShell scripts and logging their errors for display in Visual studio. Here is how it was done.

Sample Project: Noc.PsBuild.zip (9.35 KB)

Creating The MSBuild Task

(The sample project uses Visual Studio 2008 and targets the .NET Framework 3.5. I believe the solution can be adapted forward to VS 2010 and backward to VS 2005. The MSBuild task also requires PowerShell 2.0.)

The project references the following assemblies for MSBuild and for PowerShell automation:

  • Microsoft.Build.Framework (GAC)
  • Microsoft.Build.Utilities.v3.5 (GAC)
  • System.Management.Automation (from C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0)

Executing a PowerShell script from an MSBuild task turned out to be fairly easy. I started with a task skeleton that includes a property to specify the path to the PowerShell script.

using System.IO;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class PsBuildTask : Task
{
   
[Required]
   
public string ScriptPath { get; set; }

   
public override bool Execute()
   
{
       
// ...
   
}
}

The class inherits from Task, which provides all the infrastructure for generating compiler errors and warnings via the Log property. The Execute() method should return false if the task is considered to fail. In this case, it should return false if a script contains errors. All that remains is to fire up the PowerShell runtime and execute the specified script.

The following code snippet starts the PowerShell runtime, adds a dot-sourcing command to the pipeline, executes the pipeline, and retrieves the errors.

// create Powershell runspace
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace
.Open();

// create a pipeline and feed it the script text
Pipeline pipeline = runspace.CreatePipeline();
pipeline
.Commands.AddScript(". " + ScriptPath);

// execute the script and extract errors
pipeline
.Invoke();
var errors = pipeline.Error;

The errors object can read errors from the pipeline, if they exist, using one of the Read() overloads. The next snippet iterates through the errors, if any, then retrieves the error record and logs to the Log utility. This particular LogError() overload allows the specification of which file, line, and column is responsible for the error. That is key to getting the double-click send-me-to-the-error behavior in Visual Studio.

// log an MSBuild error for each error.
foreach (PSObject error in errors.Read(errors.Count))
{
   
var invocationInfo = ((ErrorRecord)(error.BaseObject)).InvocationInfo;
   
Log.LogError(
       
"Script",
       
string.Empty,
       
string.Empty,
       
new FileInfo(ScriptPath).FullName,
        invocationInfo
.ScriptLineNumber,
        invocationInfo
.OffsetInLine,
       
0,
       
0,
        error
.ToString());
}

Finally, close the runtime and return true if there were no errors logged.

// close the runspace
runspace
.Close();

return !Log.HasLoggedErrors;

That's it for the MSBuild task. Now, to use the task, it must either be installed to the GAC, or placed in a known location. Once that is established, other projects can be configured to consume this new task.

Consuming the MSBuild Task

Consider, for example, a C#-based class library project (.csproj). Integrating the task in a post build event requires just a few things.

First, register the task just inside the <Project> node of the .csproj file like so:

<UsingTask TaskName="PsBuildTask"
           
AssemblyFile="..\Noc.PsBuild\bin\Debug\Noc.PsBuild.dll" />

TaskName should be the name of the task, though it appears namespace is not required. Assembly file is an absolute path to the custom MSBuild task assembly, or relative path with respect to the .csproj file. For assemblies in the GAC, you can use the AssemblyName attribute instead.

Once registered, the task can be used within pre- and post-build events. Configure a build event within the <Project> element of the .csproj file like so:

<Target Name="AfterBuild">
   
<PsBuildTask ScriptPath=".\script.ps1" />
</Target>

And that's it. When Visual Studio compiles the project, it loads the custom assembly and task object and executes the task. Errors raised by the pipeline are retrieved and reported.

Now, a final note about the sample project. Visual Studio has a nasty habit of locking the Noc.PsBuild assembly each time Noc.PsBuild.Demo builds. That means that to rebuild this solution more than once requires restarting Visual Studio. This is all well and good for a demo, but of course this project structure, where the build task is in the same solution as a dependent assembly, is not ideal for real development. Also, the solution has only the "Works on My Machine" seal of approval. There could be gotchas, and it certainly could be extended to be even more flexible. Also, the script.ps1 file may be blocked if your execution policy is RemoteSigned. You'll need to unblock it or relax the policy to get successful execution. Good luck with it.

Happy coding!


 
Categories: MSBuild | PowerShell