SharePoint Automation Gary Lapointe – Founding Partner, Aptillon, Inc.

10Feb/130

Announcing My SharePoint 2013 Custom Cmdlets

I’ve been putting this off for quite a while but I’ve finally pushed out a SharePoint 2013 build of my custom cmdlets. The reason it took so long was because I had to make a fair bit of changes to my existing stuff so that it would be easier to maintain both builds going forward. Specifically I needed to change the namespace of all the classes (which had 2010 in them) and I wanted to use a different name for the WSPs so the version wasn’t included in it either. So now I have just one WSP name for the SharePoint 2010 and SharePoint 2013 cmdlets for both Server and Foundation: Lapointe.SharePoint.PowerShell.wsp. If you previously had my old 2010 cmdlets deployed you’ll need to fully retract them before installing this new build (technically you can rename the new WSP file to the old name but I’d rather you embrace the change and just suck it up and do the dang retraction – it only takes a minute, so don’t be lazy!)

I’ve updated my downloads page to point to the correct WSP for each environment and I’ve deleted the old WSPs so if you were foolishly linking directly to my WSPs (please don’t do that) then your links are now broken. I’ve also posted the source code which has been upgraded for Visual Studio 2012 and contains a separate project for SharePoint 2010 and SharePoint 2013 (in addition to my custom MAML generator).

Another change I’ve made to help me manage these custom cmdlets better was that I got rid of my old command index page and created a new app for displaying the cmdlet details (the old page is still there, it just redirects to the new page). This new page is actually built dynamically using the PowerShell help file that I generate dynamically from the actual cmdlet classes – so for me this is pretty cool because now all the PowerShell help documentation and online documentation of each cmdlet is generated automatically so I don’t have to do anything other than provide the actual help details in the cmdlet classes themselves and I don’t do anything special to keep them in sync (just copy the help files up to my site).

At present both the SharePoint 2010 and SharePoint 2013 cmdlets are exactly the same (except for a few in code changes to make it work with 2013). I have, however, added a few new cmdlets from what was previously available and I’ll be added some more in the coming weeks (I’m hoping to start converting some of my more frequently used utility scripts and functions to cmdlets so I don’t have to keep hunting around for them). There is however, one breaking change (well, two to be exact) – the first is that I had to rename my Repair-SPSite cmdlet to Repair-SPMigratedSite because SharePoint 2013 introduces a cmdlet of the same name; the second was that I removed the gl-applytheme STSADM command as the functionality that it provided was specific to SharePoint 2007 and is no longer available (but I’m not really supporting the STSADM stuff anyways and contemplated removing them entirely but decided to leave them in, for now).

I haven’t had time to do a ton of testing of all the cmdlets on SharePoint 2013 - there’s just too many of them and I don’t make any money on these things so it’s not a high priority – so, as always, your feedback is appreciated and I’ll do my best to fix any bugs that are discovered but I can’t promise when I’ll get to them.

Happy PowerShelling!

-Gary

15Jun/124

Replace SharePoint 2010 Web Parts by Type

Have you ever found yourself in a situation where you needed to replace all occurrences of one web part type with another web part type? No? Consider this scenario: you are using the out of the box content query web part and you discover one of the numerous bugs with this web part or decide that you want to ensure that a specific XSLT file is always used or something like that, so you decide to create a custom content query web part by sub-classing the out of the box one; now you deploy your custom web part and remove the out of the box one from the web part gallery so that any new instance will now be based on your custom type. So this is great, you’ve accomplished your goals and have implemented one of my personal best practices (don’t use the out of the box content query web part and instead use a custom implementation). But now what do you do with the potentially hundreds of existing instances that are deployed on pages throughout your Farm? Well, you need to somehow replace those instances with instances of your new type. For this, PowerShell is your friend!

I’ve encountered this specific scenario as well as numerous other ones (replacing crappy web parts written by other consultants with new versions in different solutions, etc.) and have written lots of different scripts which accomplish this goal. But today I decided I was tired of creating and managing all these scripts so I went ahead and created a custom cmdlet that could achieve what I needed for a given SPFile with a single command: Replace-SPWebPartType.

The Replace-SPWebPartType cmdlet accepts an URL to a web part page (or an instance of an SPFile object) and a string or Type object representing the type of web part to replace and what to replace it with; you can further restrict what web parts are updated by providing a web part title to filter on and you can pass in additional properties to set via a Hashtable object (closed web parts are ignored). The full help for the cmdlet can be seen below:

NAME
    Replace-SPWebPartType

SYNOPSIS
    Replaces instances of one web part type with another web part type.

SYNTAX
    Replace-SPWebPartType [-File] <SPFilePipeBind> -OldType <TypePipeBind> -NewType <TypePipeBind> [-Title <String>] [-Properties <Hashtable>] [-Publish <SwitchParameter>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]


DESCRIPTION
    Replaces instances of one web part type with another web part type.

    Copyright 2011 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    > http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.


PARAMETERS
    -File <SPFilePipeBind>
        The URL to a web part page or an instance of an SPFile object.

        Required?                    true
        Position?                    1
        Default value
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false

    -OldType <TypePipeBind>
        The web part type to replace.

        Required?                    true
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -NewType <TypePipeBind>
        The web part type to replace the old type with.

        Required?                    true
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -Title [<String>]
        The web part title to restrict the replacement to.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -Properties [<Hashtable>]
        Additional properties to set or override after copying the old web part properties.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -Publish [<SwitchParameter>]
        If specified the page will be published after adjusting the Web Part.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -AssignmentCollection [<SPAssignmentCollection>]
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.

        When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false

    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".

INPUTS

 

OUTPUTS

 

NOTES


        For more information, type "Get-Help Replace-SPWebPartType -detailed". For technical information, type "Get-Help Replace-SPWebPartType -full".

    ------------------EXAMPLE------------------

    PS C:\> Replace-SPWebPartType -File "http://server_name/pages/default.aspx" -OldType "Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart, Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" -NewType "MyContentByQueryWebPart, MyCompany.SharePoint.WebParts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4ec4b9177b831752" -Publish


    This example replaces all instances of the web part who's class name is ContentByQueryWebPart with the web part who's class name is MyContentByQueryWebPart.


RELATED LINKS
    Get-SPFile

   

 

As previously stated, I’ve created a custom PipeBind object for the type parameters so that you can pass in either a string or an actual type object and you don’t always have to provide the full assembly details. So you could call the cmdlet like this (as opposed to using a string as shown in the example included with the help text):

$oldType = [Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart]
$newType = [Lapointe.SharePoint.MyContentByQueryWebPart]
Replace-SPWebPartType -File http://demo/Pages/default.aspx -OldType $oldType -NewType $oldType

 

The preceding examples all work with a single file but you could of course wrap this in a loop to update all web parts in a specific Library, Site, Site Collection, Web Application or the entire Farm. Additionally, the cmdlet supports providing the –WhatIf parameter so you can see what changes would be made without it actually changing anything. And finally, it also has support for web parts in content fields so when it deletes the old web part it will update the content to make sure it points to the new web part. (Note that I’m simply using reflection to iterate through all the properties of the old web part and to set those same properties on the new web part – if the property doesn’t exist on the new web part then it is ignored).

So to wrap up a final example here is a short script which updates all publishing pages where appropriate:

$newType = [Lapointe.SharePoint.MyContentByQueryWebPart]
$oldType = [Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart]
foreach ($site in (Get-SPWebApplication).Sites) {
    foreach ($web in $site.AllWebs) {
        foreach ($page in (Get-SPPublishingPage -Web $web)) {
            $gc = Start-SPAssignment
            try {
                $mgr = $gc | Get-SPLimitedWebPartManager $page.Uri.ToString() -ErrorAction SilentlyContinue
            } catch {
                Write-Warning "Error retrieving mgr: $($page.Uri.ToString())"
                continue
            }
            if ($mgr -eq $null) { continue }
            $changeNeeded = $false
            foreach ($wp in $mgr.WebParts) {
                if ($wp -eq $null) { continue }
                if ($wp.GetType() –eq $oldType) {
                    Write-Host "Found CQWP => $($page.Uri.ToString())::$($wp.Title)"
                    $changeNeeded = $true
                }
            }
            if ($changeNeeded) {
                Write-Host "About to make a change to $($page.Uri.ToString())" -ForegroundColor Green
                Replace-SPWebPartType -File $page.Uri.ToString() -OldType $oldType -NewType $newType -Publish
            }
            
            $gc | Stop-SPAssignment
        }
        $web.Dispose()
    }
    $site.Dispose()
}
16Mar/125

Exporting and Importing SharePoint 2010 Terms

Ever had the need to migrate Terms from the Managed Metadata Term Store from one environment to another? Do you find the flat, CSV, import approach provided out of the box to be insufficient (especially with its lack of support for alternate labels)? When I first started working with Terms over two years ago I was extremely frustrated by the lack of export and import capabilities so I decided to solve the problem myself by creating two cmdlets, Export-SPTerms and Import-SPTerms. I’ve actually had these cmdlets publicly available for about two years now but I suspect very few people actually know they exist so I thought I’d put together this short post just to highlight them (I also recently pushed out an update which adds support for Site Collection scoped Groups if you have SP1 deployed).

I don’t want to go into a lot of detail regarding how these cmdlets work as they’re really very simple and the bulk of the code is just about iterating through the Term Store structure and turning the various objects into an XML structure (you can download the code from my downloads page). So with brevity in mind, here’s the full help for the Export-SPTerms cmdlet:

NAME
    Export-SPTerms
   
SYNOPSIS
    Export the Managed Metadata Terms.
   
SYNTAX
    Export-SPTerms [-TaxonomySession] <SPTaxonomySessionPipeBind> [[-OutputFile] <String>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
   
    Export-SPTerms [-TermStore] <SPTaxonomyTermStorePipeBind> [[-OutputFile] <String>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
   
    Export-SPTerms [-Group] <SPTaxonomyGroupPipeBind> [[-OutputFile] <String>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
   
    Export-SPTerms [-TermSet] <SPTaxonomyTermSetPipeBind> [[-OutputFile] <String>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
   
    Export-SPTerms [-Term] <SPTaxonomyTermPipeBind> [[-OutputFile] <String>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
   
   
DESCRIPTION
    Export the Managed Metadata Terms.
   
    Copyright 2011 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    >http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.
   

PARAMETERS
    -TaxonomySession <SPTaxonomySessionPipeBind>
        The TaxonomySession object containing the Term Stores to export.
       
        Required?                    true
        Position?                    1
        Default value               
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
       
    -TermStore <SPTaxonomyTermStorePipeBind>
        The TermStore object containing the terms to export.
       
        Required?                    true
        Position?                    1
        Default value               
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false
       
    -Group <SPTaxonomyGroupPipeBind>
        The Group object containing the terms to export.
       
        Required?                    true
        Position?                    1
        Default value               
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false
       
    -TermSet <SPTaxonomyTermSetPipeBind>
        The TermSet object containing the terms to export.
       
        Required?                    true
        Position?                    1
        Default value               
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false
       
    -Term <SPTaxonomyTermPipeBind>
        The Term object containing the terms to export.
       
        Required?                    true
        Position?                    1
        Default value               
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false
       
    -OutputFile [<String>]
        The path to the file to save the terms to.
       
        Required?                    false
        Position?                    2
        Default value               
        Accept pipeline input?       false
        Accept wildcard characters?  false
       
    -AssignmentCollection [<SPAssignmentCollection>]
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.
       
        When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.
       
        Required?                    false
        Position?                    named
        Default value               
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
       
    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".
   
INPUTS
   
   
   
OUTPUTS
   
   
   
NOTES
   
   
    For more information, type "Get-Help Export-SPTerms -detailed". For technical information, type "Get-Help Export-SPTerms -full".
   
    ------------------EXAMPLE 1-----------------------
   
    PS C:\> Export-SPTerms -TaxonomySession "http://site/" -OutputFile "c:\terms.xml"
   
   
    This example exports the terms for all term stores associated with the site and saves to c:\terms.xml.
   
    ------------------EXAMPLE 2-----------------------
   
    PS C:\> Export-SPTerms -Group (Get-SPTaxonomySession -Site "http://site/").TermStores[0].Groups[0] -OutputFile "c:\terms.xml"
   
   
    This example exports the first Group of the first Term Store and saves to c:\terms.xml.
   
   
RELATED LINKS
    Import-SPTerms
    Get-SPTaxonomySession

 

Now lets look at the Import-SPTerms cmdlet (it’s very similar):


NAME
    Import-SPTerms
   
SYNOPSIS
    Import the Managed Metadata Terms.
   
SYNTAX
    Import-SPTerms [-TaxonomySession] <SPTaxonomySessionPipeBind> [-InputFile] <XmlDocumentPipeBind> [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
   
    Import-SPTerms [-ParentTermStore] <SPTaxonomyTermStorePipeBind> [-InputFile] <XmlDocumentPipeBind> [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
   
    Import-SPTerms [-ParentGroup] <SPTaxonomyGroupPipeBind> [-InputFile] <XmlDocumentPipeBind> [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
   
    Import-SPTerms [-ParentTermSet] <SPTaxonomyTermSetPipeBind> [-InputFile] <XmlDocumentPipeBind> [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
   
    Import-SPTerms [-ParentTerm] <SPTaxonomyTermPipeBind> [-InputFile] <XmlDocumentPipeBind> [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
   
   
DESCRIPTION
    Import the Managed Metadata Terms.
   
    Copyright 2011 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    > http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.
   

PARAMETERS
    -TaxonomySession <SPTaxonomySessionPipeBind>
        The TaxonomySession object to import Term Stores into.
       
        Required?                    true
        Position?                    1
        Default value               
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
       
    -ParentTermStore <SPTaxonomyTermStorePipeBind>
        The TermStore object to import Groups into.
       
        Required?                    true
        Position?                    1
        Default value               
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false
       
    -ParentGroup <SPTaxonomyGroupPipeBind>
        The Group object to import Term Sets into.
       
        Required?                    true
        Position?                    1
        Default value               
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false
       
    -ParentTermSet <SPTaxonomyTermSetPipeBind>
        The TermSet object to import Terms into.
       
        Required?                    true
        Position?                    1
        Default value               
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false
       
    -ParentTerm <SPTaxonomyTermPipeBind>
        The Term object to import Terms into.
       
        Required?                    true
        Position?                    1
        Default value               
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false
       
    -InputFile <XmlDocumentPipeBind>
        The path to the file containing the terms to import or an XmlDocument object or XML string.
       
        Required?                    true
        Position?                    2
        Default value               
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
       
    -AssignmentCollection [<SPAssignmentCollection>]
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.
       
        When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.
       
        Required?                    false
        Position?                    named
        Default value               
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
       
    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".
   
INPUTS
   
   
   
OUTPUTS
   
   
   
NOTES
   
   
    For more information, type "Get-Help Import-SPTerms -detailed". For technical information, type "Get-Help Import-SPTerms -full".
   
    ------------------EXAMPLE 1-----------------------
   
    PS C:\> Import-SPTerms -ParentTermStore (Get-SPTaxonomySession -Site "http://site/").TermStores[0] -InputFile "c:\terms.xml"
   
   
    This example imports the Group from c:\terms.xml to the first Term Store.
   
    ------------------EXAMPLE 2-----------------------
   
    PS C:\> Import-SPTerms -TaxonomySession "http://site/" -InputFile "c:\terms.xml"
   
   
    This example imports the terms from c:\terms.xml to the Term Store associated with http://site.
   
   
RELATED LINKS
    Export-SPTerms
    Get-SPTaxonomySession

 

Using these cmdlets I can easily export terms from one environment, such as production, to another environment, such as my test environment. This avoids the need to have backup and restore the database associated with the term store (which requires removing and re-provisioning the service application. And the way I’ve written the cmdlets all the Term Set, Group, and Term IDs remain consistent and, because the exported XML is easily modified, when it comes to Site Collection scoped Groups you can do a simple search and replace to change Site Collection URLs to match the new target (note that I first check for a Site Collection with the specified URL and if not found then I use the ID and if I still can’t locate the Site Collection then I create the Group as a standard shared Group).

-Gary

22Dec/1140

Updating SharePoint 2010 User Information

One of my clients recently had an issue where a particularly high profile user (CEO) had their title spelled incorrectly in Active Directory; unfortunately the error wasn’t noticed right away and now, despite changing the information in Active Directory, SharePoint was still showing the wrong title in the People Picker when granting the user rights to a Site Collection. Fortunately I had a partial PowerShell script to fix the issue and just needed to only slightly modify it – you can see the original script on pages 299 and 300 of my book. So before I show the modified script it’s first important to understand the problem and why I needed to use a script and why what I had in the book is somewhat incomplete.

Whenever you grant a user rights to a Site Collection or when that user creates/updates/deletes any item within a Site Collection, an entry for the user will be added to a hidden user information list, if not already there. This “User Information List” is located at http://<SiteCollectionUrl>/_catalogs/users/detail.aspx:

SNAGHTMLa064e71

By looking at this list you can see that several key pieces of information are stored here – unfortunately, when you change this information in Active Directory the information stored here is not updated (even after running a full or incremental import via UPS). To complicate matters there is no way to edit the information via the browser, thus the need for a PowerShell script. If you click the user’s name you’ll see the additional properties, including an “Edit Item” option, however, the edit dialog is simply a read-only display of the username, helpful right?:

SNAGHTMLa089b49

So let’s first consider the scenario that my book addresses and assume that a user had had their name and/or email address changed. To accommodate this scenario we simply use the Set-SPUser cmdlet along with the -SyncFromAD parameter. The following script is taken directly from my book and simply iterates through all Site Collections and calls the Set-SPUser cmdlet for the provided user:

function Sync-SPUser([string]$userName) {
  Get-SPSite -Limit All | foreach {
    $web = $_.RootWeb
    if ($_.WebApplication.UseClaimsAuthentication) {
      $claim = New-SPClaimsPrincipal $userName -IdentityType WindowsSamAccountName
      $user = $web | Get-SPUser -Identity $claim -ErrorAction SilentlyContinue
    } else {
      $user = $web | Get-SPUser -Identity $userName -ErrorAction SilentlyContinue
    }
    if ($user -ne $null) {
      $web | Set-SPUser -Identity $user -SyncFromAD
    }
    $web.Dispose()
    $_.Dispose()
  }
}

 

Before I make any changes to demonstrate this script and the modifications we’ll make to it, let’s first see how my user is currently set in the Site Collection:

image

And as shown in the People Picker:

SNAGHTMLa14e91b

Note the “Name”/”Display Name”, “Work e-mail”/”E-Mail”, and “Title” fields.

Now I’ll change these values in Active Directory (make the “p” in my last name capitalized, change the title, and set the email) and then run the script (I saved the script as Sync-SPUser.ps1):

SNAGHTMLa17239b

(Note that lowercase “p” is the correct spelling for my name, just in case you were wondering Smile). Now if we look at the user details in the Site Collection and the People Picker we should see the following:

image

SNAGHTMLa1a344d

Notice that the the “Name” / “Display Name” and “Work e-mail” / “E-Mail” fields were updated but not the “Title” field. This is because the Set-SPUser cmdlet and -SyncFromAD parameter only updates these two fields. So how do you update the remaining fields? We simply need to add some code to our function which will grab the SPListItem corresponding to the user from the hidden “User Information List” and then update the corresponding fields manually. The following modified script does this for the “Title” field (note that I’ve changed the function signature to take the title in as a parameter):

function Sync-SPUser([string]$userName, [string]$title) {
  Get-SPSite -Limit All | foreach {
    $web = $_.RootWeb
    if ($_.WebApplication.UseClaimsAuthentication) {
      $claim = New-SPClaimsPrincipal $userName -IdentityType WindowsSamAccountName
      $user = $web | Get-SPUser -Identity $claim -ErrorAction SilentlyContinue
    } else {
      $user = $web | Get-SPUser -Identity $userName -ErrorAction SilentlyContinue
    }
    if ($user -ne $null) {
      $web | Set-SPUser -Identity $user -SyncFromAD
      
      $list = $web.Lists["User Information List"]
      $query = New-Object Microsoft.SharePoint.SPQuery
      $query.Query = "<Where><Eq><FieldRef Name='Name' /><Value Type='Text'>$userName</Value></Eq></Where>"
      foreach ($item in $list.GetItems($query)) {
        $item["JobTitle"] = $title
        $item.SystemUpdate()
      }
    }
    $web.Dispose()
    $_.Dispose()
  }
}

The changes to the original function have been highlighted. Note that the internal field name for the “Title” field is “JobTitle” and that is what we are using to set the Title. Now if we run this modified script we should see the Title field updated:

SNAGHTMLa21bc70

SNAGHTMLa236af7

Okay, so what about the other fields (Department, Mobile Number, etc.)? You can see what fields are available to edit by running the following:

SNAGHTMLa265249

In the preceding example I’m grabbing a specific item (in this case the item corresponding to my user) so that I can see the internal field names in context with the data stored by the field – this helps to make sure that I grab the correct field name (i.e., “JobTitle” vs. “Title”). Now you can just add additional fields to update right before the call to SystemUpdate() – simply follow the pattern established for the title field.

So, add this guy to your script library and you’ll be good to go next time someone changes their name, email, or job title.

-Gary

20Aug/1111

Resetting SharePoint 2010 Themes – Part 2, the Reset-SPTheme cmdlet

Yesterday I threw up a quick post showing how to reset a SharePoint 2010 theme using a reasonably simple Windows PowerShell script. In that post I promised that I’d convert the script to a cmdlet and make it part of my downloadable extensions. Well, as promised I’ve updated my extensions so that they now include a Reset-SPTheme cmdlet. I added on minor enhancement over the previously shown script in that I allow you to pass in either an SPSite or an SPWeb object and by default it will not force all child webs to inherit from the relevant SPWeb object. This way, if you have a child Site with it’s own theme it won’t wipe out that theme. If you have multiple Sites with a custom theme setting within a Site Collection then you’ll want to provide the -Site parameter and pass in an SPSite reference – this will result in all Sites with custom themes within the Site Collection to be reset. If you only wish to reset a single Site then use the -Web parameter and pass in a SPWeb reference.

Here’s the full help for the Reset-SPTheme cmdlet:

NAME
    Reset-SPTheme

SYNOPSIS
    Resets a theme by applying all user specified theme configuration settings to the original source files. This is particularly helpful when the original source files have changed to a Feature upgrade.

SYNTAX
    Reset-SPTheme [-Web] <SPWebPipeBind> [-SetSubWebsToInherit <SwitchParameter>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]

    Reset-SPTheme [-Site] <SPSitePipeBind> [-SetSubWebsToInherit <SwitchParameter>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]

DESCRIPTION
    Resets a theme by applying all user specified theme configuration settings to the original source files. This is particularly helpful when the original source files have changed to a Feature upgrade.

    Copyright 2011 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    >
http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.

PARAMETERS
    -Web <SPWebPipeBind>
        Specifies the URL or GUID of the Web containing the theme to reset.

        The type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid name of Microsoft SharePoint Foundation 2010 Web site (for example, MySPSite1); or an instance of a valid SPWeb object.

        Required?                    true
        Position?                    1
        Default value
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false

    -Site <SPSitePipeBind>
        The site containing the theme to reset.

        The type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid URL, in the form http://server_name; or an instance of a valid SPSite object.

        Required?                    true
        Position?                    1
        Default value
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false

    -SetSubWebsToInherit [<SwitchParameter>]
        If specified, all child webs will be reset to inherit the theme of the specified web or root web.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -AssignmentCollection [<SPAssignmentCollection>]
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.

        When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false

    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".

INPUTS

OUTPUTS

NOTES

        For more information, type "Get-Help Reset-SPTheme -detailed". For technical information, type "Get-Help Reset-SPTheme -full".

    ------------------EXAMPLE 1-----------------------

    PS C:\> Get-SPSite http://server_name | Reset-SPTheme -SetSubWebsToInherit

    This example resets the theme for the site collection http://server_name and resets all child webs to inherit from the root web.

    ------------------EXAMPLE 2-----------------------

    PS C:\> Get-SPWeb http://server_name/sub-web | Reset-SPTheme

    This example resets the theme for the web http://server_name/sub-web.

RELATED LINKS
    Get-SPWeb
    Get-SPSite

 

In the following example I’m resetting the theme(s) for an entire Site Collection. If one any child Sites within the Site Collection have a custom theme then they’ll be updated, not just the root (inheritance will not be changed):

PS C:\> Reset-SPTheme -Site http://example.com

In this next example I’m resetting all child Sites to inherit whatever theme has been specified for the root Site and I’m updating the root Site’s theme with changes to the source files:

PS C:\> Reset-SPTheme -Site http://example.com -SetSubWebsToInherit

For this last example I’m resetting the theme of a specific sub-Site:

PS C:\> Reset-SPTheme -Web http://example.com

As you can see, this is pretty easy to use and, if you’re deploying your branding via Features and you have theme support then a cmdlet like this can be quite critical when you need to push out updates to that brand.

26Jun/118

Getting (and taking ownership of) Checked Out Files using Windows PowerShell

Often when I’m working on a project I need to generate a list of all checked out files and provide that to my client just prior to release to production. Sometimes the client will manually inspect each of them and act as they see fit and other times they’ll ask me to just batch publish all of them (for which I use my Publish-SPListItems cmdlet). So, how do I generate the report for the client? It’s actually pretty easy using PowerShell and a couple of quick loops. Here’s an example that loops through every Site Collection in the Farm and generates a nice report:

function Get-CheckedOutFiles() {
    foreach ($web in (Get-SPSite -Limit All | Get-SPWeb -Limit All)) {
        Write-Host "Processing Web: $($web.Url)..."
        foreach ($list in ($web.Lists | ? {$_ -is [Microsoft.SharePoint.SPDocumentLibrary]})) {
            Write-Host "`tProcessing List: $($list.RootFolder.ServerRelativeUrl)..."
            foreach ($item in $list.CheckedOutFiles) {
                if (!$item.Url.EndsWith(".aspx")) { continue }
                $hash = @{
                    "URL"=$web.Site.MakeFullUrl("$($web.ServerRelativeUrl.TrimEnd('/'))/$($item.Url)");
                    "CheckedOutBy"=$item.CheckedOutBy;
                    "CheckedOutByEmail"=$item.CheckedOutByEmail
                }
                New-Object PSObject -Property $hash
            }
            foreach ($item in $list.Items) {
                if ($item.File.CheckOutStatus -ne "None") {
                    if (($list.CheckedOutFiles | where {$_.ListItemId -eq $item.ID}) -ne $null) { continue }
                    $hash = @{
                        "URL"=$web.Site.MakeFullUrl("$($web.ServerRelativeUrl.TrimEnd('/'))/$($item.Url)");
                        "CheckedOutBy"=$item.File.CheckedOutByUser;
                        "CheckedOutByEmail"=$item.File.CheckedOutByUser.Email
                    }
                    New-Object PSObject -Property $hash
                }
            }
        }
        $web.Dispose()
    }
}
Get-CheckedOutFiles | Out-GridView

Running the above will generate a fairly nice report with URLs and usernames and whatnot; you could also use the Export-Csv cmdlet to dump the results to a CSV file that you can then hand off to your end-users. One cool thing to point out about this is that it will also show you files that you normally can’t see – that is files that have been created by other users but have never had a check in. This is actually pretty cool and I stumbled upon this when trying to fine tune my Publish-SPListItems cmdlet. You see, if the file has never been checked in then iterating through the SPListItemCollection object will not reveal the item (or file I should say); this meant that my cmdlet, as it was previously written, was missing a bunch of files. So to work around this all I had to do was add an additional loop to iterate over the collection returned by the SPDocumentLibrary’s CheckedOutFiles property. For each SPCheckedOutFile object in that collection I then call TakeOverCheckOut() to grab the checked out file so that I can then publish.

I use this enough that I decided to turn it into a cmdlet that is now part of my custom extensions. Like the above script, I return back a custom object that contains the full URLs and other  useful information (such as the List, Site, and Site Collection identifiers). I also exposed a TakeOverCheckOut() and Delete() method which simply calls Microsoft’s implementation of those methods.

I called this cmdlet Get-SPCheckedOutFiles (note that I’d previously released this cmdlet under the name Get-SPFilesCheckedOut but have since reworked and renamed that original implementation).

Here’s the full help for the cmdlet:

PS C:\Users\spadmin> help Get-SPCheckedOutFiles -full

NAME
Get-SPCheckedOutFiles

SYNOPSIS
Retrieves check out details for a given List, Web, or Site.

SYNTAX
Get-SPCheckedOutFiles [-Site] <SPSitePipeBind> [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]

Get-SPCheckedOutFiles [-Web] <SPWebPipeBind> [-ExcludeChildWebs <SwitchParameter>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]

Get-SPCheckedOutFiles [[-Web] <SPWebPipeBind>] [-List] <SPListPipeBind> [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]


DESCRIPTION
Retrieves check out details for a given List, Web, or Site.

Copyright 2010 Falchion Consulting, LLC
> For more information on this cmdlet and others:
>
http://blog.falchionconsulting.com/
> Use of this cmdlet is at your own risk.
> Gary Lapointe assumes no liability.

PARAMETERS
-Site <SPSitePipeBind>
Specifies the URL or GUID of the Site to inspect.

The type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid URL, in the form
http://server_name; or an instance of a valid SPSite object.

Required? true
Position? 1
Default value
Accept pipeline input? true (ByValue, ByPropertyName)
Accept wildcard characters? false

-Web <SPWebPipeBind>
Specifies the URL or GUID of the Web to inspect.

The type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid URL, in the form
http://server_name; or an instance of a valid SPWeb object.

Required? true
Position? 1
Default value
Accept pipeline input? true (ByValue, ByPropertyName)
Accept wildcard characters? false

-List <SPListPipeBind>
The list whose checked out files are to be returned.

The value must be a valid URL in the form
http://server_name/lists/listname or /lists/listname. If a server relative URL is provided then the Web parameter must be provided.

Required? true
Position? 1
Default value
Accept pipeline input? true (ByValue, ByPropertyName)
Accept wildcard characters? false

-ExcludeChildWebs [<SwitchParameter>]
Excludes all child sites and only considers the specified site.

Required? false
Position? named
Default value
Accept pipeline input? false
Accept wildcard characters? false

-AssignmentCollection [<SPAssignmentCollection>]
Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.

When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.

Required? false
Position? named
Default value
Accept pipeline input? true (ByValue)
Accept wildcard characters? false

<CommonParameters>
This cmdlet supports the common parameters: Verbose, Debug, ErrorAction, ErrorVariable, WarningAction, WarningVariable, OutBuffer and OutVariable. For more information, type, "get-help about_commonparameters".

INPUTS

OUTPUTS

NOTES

For more information, type "Get-Help Get-SPCheckedOutFiles -detailed". For technical information, type "Get-Help Get-SPCheckedOutFiles -full".

------------------EXAMPLE------------------

PS C:\> Get-SPCheckedOutFiles -Site "
http://server_name/"


This example outputs a list of files that are checked out for the given Site Collection


RELATED LINKS
Get-SPFile

 

In the following example I’m retrieving pages from the root Pages library that are checked out:

image

In this example I am running the cmdlet as the aptillon\spadmin user and I’m now able to see the checkout by the user aptillon\glapointe. I ran the cmdlet twice so you could see the default tabular view as well as the more detailed view. Again, you could easily use the Export-Csv cmdlet to dump this information to a file that you can provide your end-users.

I hope you find this cmdlet useful – it personally has proven invaluable to me, particularly when working on anonymous access internet sites as end-users are notorious about creating pages and not getting them checked in.

P.S. With this release the Publish-SPListItems cmdlet has been updated to now consider files that don’t have any existing check-ins.

25Jun/1132

Programmatically Setting SharePoint 2010 Calendar Overlays

I recently did a project where my client needed several calendars provisioned via a Feature Receiver when a particular type of Site Collection was created; they had one primary calendar and they wanted all the other calendars to be overlaid onto the primary one using SharePoint 2010’s Calendar overlay capabilities.

Here’s a quick summary of this feature if you’re not familiar with it. When you are looking at a calendar you should notice that there is a ribbon button titled “Calendar Overlay”:

image

Clicking this button brings you to the Calendar Overlay Settings page; from this page you can add or edit the overlay calendars. Clicking “New Calendar” brings you to an application page where you can define what calendar to overlay:

image

As you can see from the above screenshot, you can add not just a SharePoint calendar as an overlay but also an Exchange calendar. After configuring calendar overlays you will see overlaid items when looking at the month view for the calendar:

SNAGHTML16503622

In this example, the pink item is coming from the overlay calendar. Overlay items are added dynamically after the page loads (some JavaScript and Ajax take care of loading the items and rendering them on the page).

So that’s the basics from the end-users perspective. Now what about the technical details? Well, there’s not a lot of information out there that describes how this is done but here’s the gist of it – there’s a simple XML structure that is stored in the SPView’s CalendarSettings property; this property defines the overlays. Once you know that the rest is just a matter of figuring out what that structure looks like. The easiest way to start is to simply go through the browser and set up one or two overlays and then use some simple PowerShell to dump out that XML:

SNAGHTML16554e82[4]

Here’s a better view of what that XML looks like:

<AggregationCalendars>
<AggregationCalendar Id="{26ddb82c-9e2b-4c5d-9b7e-4ee25cf5c357}"
Type="SharePoint"
Name="My Overlay Calendar"
Description=""
Color="5"
AlwaysShow="False"
CalendarUrl="/Lists/MyOverlayCalendar/calendar.aspx">
<Settings WebUrl="http://demo"
ListId="{428bd2cb-a32d-4867-b658-6498158636a8}"
ViewId="{09928cd4-9a5e-44ed-9bf2-dfe1fc85661b}"
ListFormUrl="/Lists/MyOverlayCalendar/DispForm.aspx" />
</AggregationCalendar>
</AggregationCalendars>

I want to call particular attention to the WebUrl attribute of the Settings element; this value *must* be the full URL of the SPWeb object that contains the overlay calendar list. Okay, you’re thinking, not a huge deal, SharePoint stores the full URL for lots of things and it doesn’t really pose issues right? WRONG! Think about a scenario where you have an extended web application. So in my example I have an authoring site located at http://demo and I’ve extended this site for anonymous access under the URL http://demo.aptillon.com. Due to what I consider a design flaw with the overlays, the overlay feature will only work when the web application you are accessing the site as matches the web application defined for the WebUrl attribute. So if I were to try and access my overlay using the anonymous site I’d get the following error:

image

And of course, due to this error, the overlays will not show up. So even if I’ve only changed the protocol (http to https) I’d still get this same error. This means that, effectively, calendar overlays using SharePoint lists will only work under the context of the Web Application (and protocol) from which the overlay was defined. (I’ve torn through the code that does this and it’s something that Microsoft should be very embarrassed about – very poor performance and just flat out horribly implemented. Okay, I digress, let’s get back to the details.

Another thing you’ll want to do to understand this XML structure is to look at the code that constructs it. There’s two places to look and both require Reflector or some equivalent disassembler; the first is the SerializeAccessors() method of the Microsoft.SharePoint.ApplicationPages.Calendar.CalendarAccessorManagerImpl class. This method takes the properties provided to it and constructs the XML structure shown above. So where are these properties set? For that we need to look at the BtnOk_Click() method of the Microsoft.SharePoint.ApplicationPages.AggregationCustomizePage class. I’m not going to show the details of these methods here but suffice it to say these methods have everything you need to understand this structure.

As I previously noted, for my particular client I needed to set several overlays within a Feature Activated event; to make this easier (because there was technically several places I had to do this) I created a simple method that I could call; this method took in my target list and the list I wanted to overlay as well as several other properties. For this post I’ve taken that code and created a modified version of it which supports adding Exchange-based calendars. Here’s that code:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Xml;
using Microsoft.SharePoint;

namespace Lapointe.SharePoint2010.Automation.Common.Lists
{
    public enum CalendarOverlayColor
    {
        LightYellow = 1,
        LightGreen = 2,
        Orange = 3,
        LightTurquise = 4,
        Pink = 5,
        LightBlue = 6,
        IceBlue1 = 7,
        IceBlue2 = 8,
        White = 9
    }

    public class SetListOverlay
    {
        public static void AddCalendarOverlay(SPList targetList, string viewName, string owaUrl, string exchangeUrl, string overlayName, string overlayDescription, CalendarOverlayColor color, bool alwaysShow, bool clearExisting)
        {
            AddCalendarOverlay(targetList, viewName, owaUrl, exchangeUrl, null, overlayName, overlayDescription, color, alwaysShow, clearExisting);
        }
        public static void AddCalendarOverlay(SPList targetList, string viewName, SPList overlayList, string overlayName, string overlayDescription, CalendarOverlayColor color, bool alwaysShow, bool clearExisting)
        {
            AddCalendarOverlay(targetList, viewName, null, null, overlayList, overlayName, overlayDescription, color, alwaysShow, clearExisting);
        }
        private static void AddCalendarOverlay(SPList targetList, string viewName, string owaUrl, string exchangeUrl, SPList overlayList, string overlayName, string overlayDescription, CalendarOverlayColor color, bool alwaysShow, bool clearExisting)
        {
            bool sharePoint = overlayList != null;
            string linkUrl = owaUrl;
            if (sharePoint)
                linkUrl = overlayList.DefaultViewUrl;

            SPView targetView = targetList.DefaultView;
            if (!string.IsNullOrEmpty(viewName))
                targetView = targetList.Views[viewName];

            XmlDocument xml = new XmlDocument();
            XmlElement aggregationElement = null;
            int count = 0;
            if (string.IsNullOrEmpty(targetView.CalendarSettings) || clearExisting)
            {
                xml.AppendChild(xml.CreateElement("AggregationCalendars"));
                aggregationElement = xml.CreateElement("AggregationCalendar");
                xml.DocumentElement.AppendChild(aggregationElement);
            }
            else
            {
                xml.LoadXml(targetView.CalendarSettings);
                XmlNodeList calendars = xml.SelectNodes("/AggregationCalendars/AggregationCalendar");
                if (calendars != null)
                    count = calendars.Count;
                aggregationElement = xml.SelectSingleNode(string.Format("/AggregationCalendars/AggregationCalendar[@CalendarUrl='{0}']", linkUrl)) as XmlElement;
                if (aggregationElement == null)
                {
                    if (count >= 10)
                        throw new SPException(string.Format("10 calendar ovarlays already exist for the calendar {0}.",targetList.RootFolder.ServerRelativeUrl));
                    aggregationElement = xml.CreateElement("AggregationCalendar");
                    xml.DocumentElement.AppendChild(aggregationElement);
                }
            }
            if (!aggregationElement.HasAttribute("Id"))
                aggregationElement.SetAttribute("Id", Guid.NewGuid().ToString("B", CultureInfo.InvariantCulture));

            aggregationElement.SetAttribute("Type", sharePoint ? "SharePoint" : "Exchange");
            aggregationElement.SetAttribute("Name", !string.IsNullOrEmpty(overlayName) ? overlayName : (overlayList == null ? "" : overlayList.Title));
            aggregationElement.SetAttribute("Description", !string.IsNullOrEmpty(overlayDescription) ? overlayDescription : (overlayList == null ? "" : overlayList.Description));
            aggregationElement.SetAttribute("Color", ((int)color).ToString());
            aggregationElement.SetAttribute("AlwaysShow", alwaysShow.ToString());
            aggregationElement.SetAttribute("CalendarUrl", linkUrl);

            XmlElement settingsElement = aggregationElement.SelectSingleNode("./Settings") as XmlElement;
            if (settingsElement == null)
            {
                settingsElement = xml.CreateElement("Settings");
                aggregationElement.AppendChild(settingsElement);
            }
            if (sharePoint)
            {
                settingsElement.SetAttribute("WebUrl", overlayList.ParentWeb.Site.MakeFullUrl(overlayList.ParentWebUrl));
                settingsElement.SetAttribute("ListId", overlayList.ID.ToString("B", CultureInfo.InvariantCulture));
                settingsElement.SetAttribute("ViewId", overlayList.DefaultView.ID.ToString("B", CultureInfo.InvariantCulture));
                settingsElement.SetAttribute("ListFormUrl", overlayList.Forms[PAGETYPE.PAGE_DISPLAYFORM].ServerRelativeUrl);
            }
            else
            {
                settingsElement.SetAttribute("ServiceUrl", exchangeUrl);
            }
            targetView.CalendarSettings = xml.OuterXml;
            targetView.Update();
            /*
            <AggregationCalendars>
                <AggregationCalendar 
                    Id="{cfc22c0b-688e-4555-b1d0-784081a91464}" 
                    Type="SharePoint" 
                    Name="My Overlay Calendar"
                    Description="" 
                    Color="1" 
                    AlwaysShow="True" 
                    CalendarUrl="/Lists/MyOverlayCalendar/calendar.aspx">
                    <Settings 
                        WebUrl="http://demo" 
                        ListId="{4a15e596-674f-4af7-a548-0b01470e8d75}" 
                        ViewId="{594c2916-14e7-4b08-ba36-1126b825bf45}" 
                        ListFormUrl="/Lists/MyOverlayCalendar/DispForm.aspx" />
                </AggregationCalendar>
                <AggregationCalendar 
                    Id="{cfc22c0b-688e-4555-b1d0-784081a91465}" 
                    Type="Exchange" 
                    Name="My Overlay Calendar"
                    Description="" 
                    Color="1" 
                    AlwaysShow="True" 
                    CalendarUrl="<url>">
                    <Settings ServiceUrl="<url>" />
                </AggregationCalendar>
            </AggregationCalendars>
            */
        }
    }
}

I’m not going to bore you with the details of this code as all I’m doing is basic XML manipulation. I created a couple of method overloads to allow for creating SharePoint or Exchange-based overlays. So, did you notice the namespace? Yup, no point in releasing code here if I’m not going to turn it into a cmdlet Smile

I’m not sure how useful this cmdlet will be in everyday use but imagine the scenario in which you have a primary calendar on your company portal and you want to add it as an overlay on every calendar throughout portal – you could easily do this using this cmdlet. Before we get to that, let’s see the full help for the cmdlet, which I called Set-SPListOverlay:

PS C:\Users\spadmin> help Set-SPListOverlay -full

NAME
    Set-SPListOverlay
    
SYNOPSIS
    Sets calendar overlays for the given list.
    
SYNTAX
    Set-SPListOverlay -Color  -OverlayTitle  [-OverlayDescription ] -OwaUrl  -WebServiceUrl  [-TargetList]  [-ViewName ] [-DoNotAlwaysShow ] [-ClearExisting ] [-AssignmentCollection ] []
    
    Set-SPListOverlay -Color  [-OverlayList]  [-OverlayTitle ] [-OverlayDescription ] [-TargetList]  [-ViewName ] [-DoNotAlwaysShow ] [-ClearExisting ] [-AssignmentCollection ] []
    
    Set-SPListOverlay [-OverlayLists]  [-TargetList]  [-ViewName ] [-DoNotAlwaysShow ] [-ClearExisting ] [-AssignmentCollection ] []
    
    
DESCRIPTION
    Sets calendar overlays for the given list.
    
    Copyright 2010 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    > http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.
    

PARAMETERS
    -Color 
        The color to use for the overlay calendar.
        
        Required?                    true
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -TargetList 
        The calendar list to add the overlays to.
        
        Required?                    true
        Position?                    1
        Default value                
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
        
    -ViewName []
        The name of the view to add the overlays to. If not specified the default view will be used.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -OverlayList 
        The calendar list to add as an overlay.
        
        Required?                    true
        Position?                    2
        Default value                
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
        
    -OverlayLists 
        The calendar lists to add as an overlay.
        
        Required?                    true
        Position?                    2
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -OverlayTitle []
        The title to give the overlay calendar when viewed in the target calendar.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -OverlayDescription []
        The description to give the overlay calendar when viewed in the target calendar.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -OwaUrl 
        Outlook Web Access URL.
        
        Required?                    true
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -WebServiceUrl 
        Exchange Web Service URL.
        
        Required?                    true
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -DoNotAlwaysShow []
        Don't always show the calendar overlay.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -ClearExisting []
        Clear existing overlays. If not specified then all overlays will be appended to the list of existing over        lays (up until 10 - anything after 10 will be ignored)
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -AssignmentCollection []
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.
        
        When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
        
    
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".
    
INPUTS
    
    
OUTPUTS
    
    
NOTES
    
    
        For more information, type "Get-Help Set-SPListOverlay -detailed". For technical information, type "Get-Help Set-SPListOverlay -full".
    
    ------------------EXAMPLE------------------
    
    PS C:\> Get-SPList "http://server_name/lists/MyCalendar" | Set-SPListOverlay -TargetList "http://server_name/lists/MyOverlayCalendar" -Color "Pink" -ClearExisting
    
    
    This example adds the MyOverlayCalendar calendar as an overlay to the MyCalendar list.
    
    
RELATED LINKS
    Get-SPList 

You can see from the different parameter sets that I’ve made provisions for setting the overlay as a SharePoint list or an Exchange calendar; additionally, I’ve made it so that if you have an array of lists that you wish to add as an overlay you can easily pass that array in as well. So now let’s look at that example:

$mainList = Get-SPList http://demo/lists/myCalendar
foreach ($site in (Get-SPSite http://demo -Limit All)) {
    foreach ($web in $site.AllWebs) {
        foreach ($list in ($web.Lists | ? {$_.BaseTemplate -eq "Events"})) {
            if ($list.ID -eq $mainList.ID) { continue }
            Set-SPListOverlay -TargetList $list `
                -OverlayList $mainList `
                -Color "Pink" `
                -OverlayTitle "Main Portal Calendar" `
                -ClearExisting
        }
        $web.Dispose()
    }
    $site.Dispose()
}

Pretty simple huh? I’m just grabbing the primary list and then I’m looping through all my Site Collections and Sites and then grabbing all the lists that have a base template of “Events”. Once I have the list then I simply call my cmdlet – that’s it – easy right?

Okay, so what if you have a calendar with a bunch of overlays and you need to grab those calendars and do something to them? Well, I threw in a bonus cmdlet called Get-SPListOverlays. This one is really simple – it merely takes in the primary list and then calls a simple helper method that parses the XML structure and grabs each list. I’m not going to bother showing the code as it’s real basic and this post is long enough (just download the source if you’d like to see it) but I will show the cmdlet help:

PS C:\Users\spadmin> help Get-SPListOverlays -Full
NAME
    Get-SPListOverlays
    
SYNOPSIS
    Retrieve all SPList objects set as a calendar overlay on the given list.
    
SYNTAX
    Get-SPListOverlays [-Identity]  [-Web ] [-AssignmentCollection ] []
    
    
DESCRIPTION
    Retrieve all SPList objects set as a calendar overlay on the given list.
    
    Copyright 2010 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    > http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.
    

PARAMETERS
    -Identity 
        The calendar whose calendar overlays will be retrieved.
        
        The value must be a valid URL in the form http://server_name/lists/listname or /lists/listname. If a server relative URL is provided then the Web parameter must be provided.
        
        Required?                    true
        Position?                    1
        Default value                
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
        
    -Web []
        Specifies the URL or GUID of the Web containing the calendar whose overlays will be retrieved.
        
        The type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid name of Microsoft SharePoint Foundation 2010 Web site (for example, MySPSite1); or an instance of a valid SPWeb object.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
        
    -AssignmentCollection []
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.
        
        When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
        
    
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".
    
INPUTS
    
    
OUTPUTS
    
    
NOTES
    
    
        For more information, type "Get-Help Get-SPListOverlays -detailed". For technical information, type "Get-Help Get-SPListOverlays -full".
    
    ------------------EXAMPLE------------------
    
    PS C:\> $lists = Get-SPListOverlays "http://server_name/lists/mylist"
    
    
    This example retrieves the calendar overlays for the calendar at http://server_name/lists/mycalendar.
    
    
RELATED LINKS
    Get-SPList 
    Set-SPListOverlay 
    Get-SPWeb 

In the end this turned out to all be pretty simple to do but it was certainly a challenge trying to figure it all out as there’s no documentation (official or otherwise) that I’ve been able to find.

30Apr/110

Retrieving and Configuring the SharePoint 2010 Developer Dashboard using PowerShell

It’s been almost a year to the day since I’ve released my SharePoint 2010 cmdlets and, despite many good intentions to get them documented on my blog, things have just fallen by the wayside; this was primarily due to me going out on my own and writing my first book – but now that the book is done and I’ve begun to establish myself as an independent consultant, I believe it’s about time I start blogging about all these hidden cmdlets that I’ve created. So, to start I’m going to take a couple of cmdlets that I originally developed for some conference presentations; specifically Get-SPDeveloperDashboard and Set-SPDeveloperDashboard.

Before I show these two new cmdlets, let’s look at what it currently takes to retrieve and manipulate the developer dashboard using Windows PowerShell:

[Microsoft.SharePoint.Administration.SPWebService]::ContentService.DeveloperDashboardSettings

As you can see from the preceding figure, you obtain an instance of the SPDeveloperDashboardSettings object via the DeveloperDashboardSettings property of an SPWebService instance (obtained using the static ContentService property of the SPWebService class). Note that there are several properties that we can manipulate beyond just the simple DisplayLevel property that is used to enable or disable the developer dashboard (or to put it into on demand mode). Some people still like to use STSADM to change the DisplayLevel property but doing so doesn’t allow you to manipulate the other properties available; often the reason people use STSADM is because it’s slightly less verbose if all you wish to do is change the DisplayLevel property. Here’s an example of how you would do it with PowerShell:

$dds = [Microsoft.SharePoint.Administration.SPWebService]::ContentService.DeveloperDashboardSettings
$dds.DisplayLevel = "On"
$dds.Update()

So, not a whole lot of code but still more than the single STSADM line (that and people have a hard time remembering the full object path to get to the SPDeveloperDashboardSettings object – I personally can remember this easier than the STSADM key names).

Because of this slightly higher level of complexity I decided to create these cmdlets, but I also went ahead and added some PowerShell type extensions so that I could get to the developer dashboard from an SPFarm instance. I’ll examine that before we get into the cmdlets; if you download my source code you should notice a file named Lapointe.SharePoint2010.Automation.Cmdlets.Types.ps1xml in the {Project Root}\PowerShell\Types folder. Here’s the relevant contents of that file:

<?xml version="1.0" encoding="utf-8"?>
<Types>
  <Type>
    <Name>Microsoft.SharePoint.Administration.SPFarm</Name>
    <Members>
      <ScriptProperty>
        <Name>DeveloperDashboard</Name>
        <GetScriptBlock>[Microsoft.SharePoint.Administration.SPWebService]::ContentService.DeveloperDashboardSettings</GetScriptBlock>
      </ScriptProperty>
    </Members>
  </Type>
</Types>

What I’ve done here is essentially create a type extension using XML; the <Name /> element defines the full type name that you want to extend and the <Members /> element contains all the extensions. In this case I’ve added a new property named DeveloperDashboard and I provided the same script we saw previously so that the SPDeveloperDashboardSettings object will be returned. It’s important to understand that you are not limited to just get properties – you can create set properties as well as methods (type help about_types for more information about creating type extensions). With this type extension added we can now access the developer dashboard in a slightly simpler manner:

$dds = (Get-SPFarm).DeveloperDashboard

Using this approach there really isn’t a need for the Get-SPDeveloperDashboard cmdlet that I created, as the cmdlet only saves about seven characters; however, this approach isn’t obvious – what I want is users to be able to type Get-Command *dashboard* so that they can see all the cmdlets related to the developer dashboard. (Plus, I created the cmdlet originally just for demonstration purposes but it does make things a little more obvious). So now that we have the type extension out of the way, let’s take a look at the cmdlet. Here’s a dump of the full help for the Get-SPDeveloperDashboard cmdlet:

PS C:\> help Get-SPDeveloperDashboard -Full

NAME
    Get-SPDeveloperDashboard
    
SYNOPSIS
    Retrieves the Developer Dashboard Settings object.
    
SYNTAX
    Get-SPDeveloperDashboard [-AssignmentCollection <spassignmentcollection>] [<commonparameters>]
    
    
DESCRIPTION
    Retrieves the Developer Dashboard Settings object.
    
    Copyright 2010 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    > http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.

PARAMETERS
    -AssignmentCollection [<spassignmentcollection>]
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, 
        can use large amounts of memory and use of these objects in Windows PowerShell scripts requires 
        proper memory management. Using the SPAssignment object, you can assign objects to a variable 
        and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or 
        SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment 
        collection or the Global parameter is not used.
        
        When the Global parameter is used, all objects are contained in the global store. If objects are 
        not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory 
        scenario can occur.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
        
    <commonparameters>
        This cmdlet supports the common parameters: Verbose, Debug, ErrorAction, ErrorVariable, 
        WarningAction, WarningVariable, OutBuffer and OutVariable. For more information, 
        type, "get-help about_commonparameters".
    
INPUTS
    
OUTPUTS
    
NOTES
    
        For more information, type "Get-Help Get-SPDeveloperDashboard -detailed". 
        For technical information, type "Get-Help Get-SPDeveloperDashboard -full".
    
    ------------------EXAMPLE------------------
    
    PS C:\> $dash = Get-SPDeveloperDashboard
    
    This example returns back the developer dashboard settings object.
    
RELATED LINKS
    Set-SPDeveloperDashboard 

So obviously the cmdlet is pretty simple as there aren’t any parameters beyond the standard parameters (remember, the -AssignmentCollection parameter is included as part of the cmdlet base class but as the SPDeveloperDashboardSettings object is not disposable there is no reason to use it.

The code for this cmdlet is actually shorter than the help for it:

using System.Collections.Generic;
using System.Management.Automation;
using Lapointe.PowerShell.MamlGenerator.Attributes;
using Microsoft.SharePoint.PowerShell;
using Microsoft.SharePoint.Administration;

namespace Lapointe.SharePoint2010.Automation.Cmdlets.Farm
{
    [Cmdlet(VerbsCommon.Get, "SPDeveloperDashboard", SupportsShouldProcess = false),   SPCmdlet(RequireLocalFarmExist = true, RequireUserFarmAdmin = false)]
    [CmdletDescription("Retrieves the Developer Dashboard Settings object.")]
    [RelatedCmdlets(typeof(SPCmdletSetDeveloperDashboard))]
    [Example(Code = "PS C:\\> $dash = Get-SPDeveloperDashboard",       Remarks = "This example returns back the developer dashboard settings object.")]
    public class SPCmdletGetDeveloperDashboard : SPGetCmdletBaseCustom<SPDeveloperDashboardSettings>
    {
        protected override IEnumerable<SPDeveloperDashboardSettings> RetrieveDataObjects()
        {
            WriteObject(SPWebService.ContentService.DeveloperDashboardSettings);

            return null;
        }
    }
}

The following figure shows how you can call the cmdlet:

Get-SPDeveloperDashboard

Note that I’ve also added a new view for the SPDeveloperDashboardSettings object type (as shown in the first example – to see all the properties use the Select-Object cmdlet as shown in the second example). The custom views are added just like the custom type extensions – for views, however, you create another XML file which you can see in my source code under the {Project Root}\PowerShell\Format  folder. The following XML snippet illustrates the relevant portion of that file:

<?xml version="1.0" encoding="utf-8"?>
<Configuration>
  <ViewDefinitions>
   <View>
    <Name>SPDeveloperDashboardSettings</Name>
    <ViewSelectedBy>
      <TypeName>Microsoft.SharePoint.Administration.SPDeveloperDashboardSettings</TypeName>
    </ViewSelectedBy>
    <TableControl>
      <TableHeaders>
        <TableColumnHeader>
          <Label>Display Level</Label>
          <Width>14</Width>
          <Alignment>left</Alignment>
        </TableColumnHeader>
        <TableColumnHeader>
          <Label>Trace Enabled</Label>
          <Width>14</Width>
          <Alignment>left</Alignment>
        </TableColumnHeader>
        <TableColumnHeader>
          <Label>Required Permissions</Label>
          <Alignment>left</Alignment>
        </TableColumnHeader>
      </TableHeaders>
      <TableRowEntries>
        <TableRowEntry>
          <TableColumnItems>
            <TableColumnItem>
              <PropertyName>DisplayLevel</PropertyName>
            </TableColumnItem>
            <TableColumnItem>
              <PropertyName>TraceEnabled</PropertyName>
            </TableColumnItem>
            <TableColumnItem>
              <PropertyName>RequiredPermissions</PropertyName>
            </TableColumnItem>
          </TableColumnItems>
        </TableRowEntry>
      </TableRowEntries>
    </TableControl>
  </View>
 </ViewDefinitions>
</Configuration>

Okay, so we’ve made it easier to retrieve the developer dashboard, now I want to change the values in one step (because retrieving the object, changing the value, and calling Update() is just too much work). To do this I created the Set-SPDeveloperDashboard cmdlet. This cmdlet is a bit more complex in that I’ve exposed all the relevant properties of the SPDeveloperDashboardSettings object with an equivalent parameter. Here’s the full help for the cmdlet:

PS C:\> help Set-SPDeveloperDashboard -Full

NAME
    Set-SPDeveloperDashboard
    
SYNOPSIS
    Sets the Developer Dashboard Settings.
    
SYNTAX
    Set-SPDeveloperDashboard [-AutoLaunchEnabled <Boolean>] [-DisplayLevel <Off | OnDemand | On>] [-MaximumCriticalEventsToTrack <Int32>] 
    [-MaximumSQLQueriesToTrack <Int32>] [-RequiredPermissions <EmptyMask | ViewListItems | AddListItems | EditListItems | DeleteListItems |
    ApproveItems | OpenItems | ViewVersions | DeleteVersions | CancelCheckout | ManagePersonalViews | ManageLists | ViewFormPages | Open |
    ViewPages | AddAndCustomizePages | ApplyThemeAndBorder | ApplyStyleSheets | ViewUsageData | CreateSSCSite | ManageSubwebs | CreateGroups
    | ManagePermissions | BrowseDirectories | BrowseUserInfo | AddDelPrivateWebParts | UpdatePersonalWebParts | ManageWeb | 
    UseClientIntegration | UseRemoteAPIs | ManageAlerts | CreateAlerts | EditMyUserInfo | EnumeratePermissions | FullMask>] 
    [-TraceEnabled <Boolean>] [-AdditionalEventsToTrack <String[]>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
    
DESCRIPTION
    Sets the Developer Dashboard Settings.
    
    Copyright 2010 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    > http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.

PARAMETERS
    -AutoLaunchEnabled [<Boolean>]
        Indicates whether the developer dashboard can be auto launched.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -DisplayLevel [<SPDeveloperDashboardLevel>]
        Indicates whether the developer dashboard is set to Off, On, or On Demand.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -MaximumCriticalEventsToTrack [<Int32>]
        The maximum number of critical events and asserts that will be recorded in a single transaction (i.e. one request or timer job).
        If a single transaction has more than this number of asserts the remainder will be ignored. This can be set to 0 to disable
        assert tracking.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -MaximumSQLQueriesToTrack [<Int32>]
        The maximum number of SQL queries that will be recorded in a single transaction (i.e. one request or timer job). If a single
        transaction executes more than this number of requests the query will be counted but the query call stack and text will not be kept.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -RequiredPermissions [<SPBasePermissions>]
        A permission mask defining the permissions required to see the developer dashboard. This defaults to SPBasePermissions.AddAndCustomizePages.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -TraceEnabled [<Boolean>]
        Whether a link to display full verbose trace will be available at the bottom of the page when the developer dashboard is launched or not.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -AdditionalEventsToTrack [<String[]>]
        A list of URL tags to track in addition to events with severity above High.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -AssignmentCollection [<SPAssignmentCollection>]
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, 
        can use large amounts of memory and use of these objects in Windows PowerShell scripts requires 
        proper memory management. Using the SPAssignment object, you can assign objects to a variable 
        and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or 
        SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment 
        collection or the Global parameter is not used.
        
        When the Global parameter is used, all objects are contained in the global store. If objects are 
        not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory 
        scenario can occur.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
        
    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".
    
INPUTS
    
OUTPUTS
    
NOTES
    
        For more information, type "Get-Help Set-SPDeveloperDashboard -detailed". For technical information,
        type "Get-Help Set-SPDeveloperDashboard -full".
    
    ------------------EXAMPLE 1-----------------------
    
    PS C:\> Set-SPDeveloperDashboard -RequiredPermissions "ManageWeb,ManageSubwebs"
    
    This example sets the required permissions to view the developer dashboard.
    
    ------------------EXAMPLE 2-----------------------
    
    PS C:\> Set-SPDeveloperDashboard -DisplayLevel OnDemand -TraceEnabled $true
    
    This example enables the developer dashboard.
    
RELATED LINKS
    Get-SPDeveloperDashboard 

The code for this cmdlet is obviously going to be slightly longer than the Get-SPDeveloperDashboard cmdlet, but again, it’s very simple as most of the code is just for defining the parameters:

using System.Collections.Generic;
using System.Management.Automation;
using Lapointe.PowerShell.MamlGenerator.Attributes;
using Microsoft.SharePoint;
using Microsoft.SharePoint.PowerShell;
using Microsoft.SharePoint.Administration;

namespace Lapointe.SharePoint2010.Automation.Cmdlets.Farm
{
    [Cmdlet(VerbsCommon.Set, "SPDeveloperDashboard", SupportsShouldProcess = false),
    SPCmdlet(RequireLocalFarmExist = true, RequireUserFarmAdmin = false)]
    [CmdletDescription("Sets the Developer Dashboard Settings.")]
    [RelatedCmdlets(typeof(SPCmdletGetDeveloperDashboard))]
    [Example(Code = "PS C:\\> Set-SPDeveloperDashboard -DisplayLevel OnDemand -TraceEnabled $true",
        Remarks = "This example enables the developer dashboard.")]
    [Example(Code = "PS C:\\> Set-SPDeveloperDashboard -RequiredPermissions \"ManageWeb,ManageSubwebs\"",
        Remarks = "This example sets the required permissions to view the developer dashboard.")]
    public class SPCmdletSetDeveloperDashboard : SPSetCmdletBaseCustom<SPDeveloperDashboardSettings>
    {
        public SPCmdletSetDeveloperDashboard()
        {
            SPDeveloperDashboardSettings dash = SPWebService.ContentService.DeveloperDashboardSettings;
            AutoLaunchEnabled = dash.AutoLaunchEnabled;
            DisplayLevel = dash.DisplayLevel;
            MaximumCriticalEventsToTrack = dash.MaximumCriticalEventsToTrack;
            MaximumSQLQueriesToTrack = dash.MaximumSQLQueriesToTrack;
            RequiredPermissions = dash.RequiredPermissions;
            TraceEnabled = dash.TraceEnabled;
            AdditionalEventsToTrack = ((List<string>) dash.AdditionalEventsToTrack).ToArray();
        }

        [Parameter(HelpMessage = "Indicates whether the developer dashboard can be auto launched.")]
        public bool AutoLaunchEnabled { get; set; }

        [Parameter(HelpMessage = "Indicates whether the developer dashboard is set to Off, On, or On Demand.")]
        public SPDeveloperDashboardLevel DisplayLevel { get; set; }

        [Parameter(HelpMessage = "The maximum number of critical events and asserts that will be recorded in a single transaction (i.e. one request or timer job). If a single transaction has more than this number of asserts the remainder will be ignored. This can be set to 0 to disable assert tracking.")]
        public int MaximumCriticalEventsToTrack { get; set; }

        [Parameter(HelpMessage = "The maximum number of SQL queries that will be recorded in a single transaction (i.e. one request or timer job). If a single transaction executes more than this number of requests the query will be counted but the query call stack and text will not be kept. ")]
        public int MaximumSQLQueriesToTrack { get; set; }

        [Parameter(HelpMessage = "A permission mask defining the permissions required to see the developer dashboard. This defaults to SPBasePermissions.AddAndCustomizePages.")]
        public SPBasePermissions RequiredPermissions { get; set; }

        [Parameter(HelpMessage = "Whether a link to display full verbose trace will be available at the bottom of the page when the developer dashboard is launched or not.")]
        public bool TraceEnabled { get; set; }

        [Parameter(HelpMessage = "A list of URL tags to track in addition to events with severity above High. ")]
        public string[] AdditionalEventsToTrack { get; set; }

        protected override void UpdateDataObject()
        {
            SPDeveloperDashboardSettings dash = SPWebService.ContentService.DeveloperDashboardSettings;

            dash.AutoLaunchEnabled = AutoLaunchEnabled;
            dash.DisplayLevel = DisplayLevel;
            dash.MaximumCriticalEventsToTrack = MaximumCriticalEventsToTrack;
            dash.MaximumSQLQueriesToTrack = MaximumSQLQueriesToTrack;
            dash.RequiredPermissions = RequiredPermissions;
            dash.TraceEnabled = TraceEnabled;
            dash.AdditionalEventsToTrack.Clear();
            ((List<string>)dash.AdditionalEventsToTrack).AddRange(AdditionalEventsToTrack);

            dash.Update();
        }
    }
}

The following figure shows how you can call the cmdlet:

Set-SPDeveloperDashboard -DisplayLevel On -RequiredPermissions "ManageWeb","ManageSubwebs"

So that wraps up my first (very long overdue) post for my 2010 cmdlets – look for more posts coming soon as well as an update to my index page listing all the available cmdlets.

-Gary

19Apr/111

Upgrading User Profile Choice Fields to SharePoint 2010

I’m working on an upgrade (database attach) for a client of mine and I ran into something somewhat unexpected with some User Profile properties – properties that used a choice field in 2007 (fields that allowed the administrator to define a list of values that the user could pick from) were migrated to Managed Term Store based fields when upgraded. At first glance this was pretty cool; the only problem was that, though the field was converted to a Managed Term Store based field (essentially a Taxonomy field), the Terms themselves were not migrated and the user-specific values were lost. Now, I may have done something wrong during the upgrade process and if someone knows what I did wrong please let me know – that said, I decided to just throw together a couple quick scripts that I could use to export the field information (along with the users’ values for those fields) and then import the information in the new Farm.

To accomplish this I created two separate scripts, one for the 2007 Farm and one for the new 2010 Farm. The 2007 script loops through all the User Profile properties and, if the property has choices defined then it creates an XML structure that defines the property name and the associated choices; I put this in a function called Get-SPChoiceProperties. It then loops through all User Profiles and, for each identified property, creates another XML structure which defines the property name and associated values along with the account name; I put this in a function called Get-SPUserProperties. I then merge these two XML structures to create a single structure which is saved to disk.

Here’s the complete code which I store in a file called Export-UserProfileProperties.ps1 (note that the function names and script name are not really important  and the code is by no means perfect – I threw this whole thing together *very* quickly so that I could just get this task done and move on to the next task):

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.UserProfiles") | Out-Null
function Get-SPChoiceProperties() {
    $context = [Microsoft.Office.Server.ServerContext]::Default
    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager $context
    $props = $upm.GetProperties()
    [xml]$xml = "<Properties></Properties>"
    foreach ($prop in $props) {
        if ($prop.ChoiceList -eq $null) { continue }
        
        $propXml = $xml.CreateElement("Property")
        $xml.DocumentElement.AppendChild($propXml) | out-null
        $propXml.SetAttribute("Name", $prop.Name)
        $choicesXml = $xml.CreateElement("ChoiceList")
        $propXml.AppendChild($choicesXml) | out-null
        foreach ($choice in $Prop.ChoiceList.GetAllTerms($true)) {
            $choiceXml = $xml.CreateElement("Choice")
            $choicesXml.AppendChild($choiceXml) | out-null
            $choiceXml.InnerText = $choice
        }
    }
    $xml
}
function Get-SPUserProperties([string[]]$propertyNames) {
    $context = [Microsoft.Office.Server.ServerContext]::Default
    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager $context
    [xml]$xml = "<Users></Users>"
    foreach ($profile in $upm) {
        $userXml = $xml.CreateElement("User")
        $xml.DocumentElement.AppendChild($userXml) | out-null
        $userXml.SetAttribute("Account", $profile["AccountName"].Value)
        $propertyNames | % { 
            $val = ($profile[$_] -as [string[]]) -join ";"
            $userXml.SetAttribute($_, $val) 
        }
    }
    $xml
}
$propertyXml = Get-SPChoiceProperties
[string[]]$properties = $propertyXml.Properties.Property | % {$_.Name}
$userXml = Get-SPUserProperties $properties

[xml]$xml = "<ProfileData>$($propertyXml.OuterXml)$($userXml.OuterXml)</ProfileData>"
$xml.Save("c:\userdata.xml")

The next thing I needed to do was to create the script for the 2010 Farm. This script had three steps for which I created a separate function for each step:

  1. Create the Term Store Group if not present and, for each User Profile field identified in the exported XML create the Term Set and associated Terms. I store the field name and associated Term Set in a hash table that I can then use for the next steps. The function I created for this is called Create-SPProfileTermSets.
  2. For each User Profile property and associated Term Set returned by step 1, update the property with the new Term Set. The function I created for this is called Set-SPUserProfileProperties.
  3. Loop through all the users defined in the exported XML and, for each user, update the User Profile property with the appropriate Term value. The function I created for this is called Set-SPUserProfileValues.

I then load the exported XML and call these three functions in order, passing in the appropriate information as required. Here’s the completed code (again, it’s rough but it works – well, it met my needs):

function Create-SPProfileTermSets($session, [string]$groupName, [xml]$data) {
    
    $group = $session.DefaultSiteCollectionTermStore.Groups[$groupName]
    if ($group -eq $null) {
        $group = $ts.DefaultSiteCollectionTermStore.CreateGroup($groupName)
        $group.TermStore.CommitAll()
    }
    $termSets = @{}
    $data.ProfileData.Properties.Property | % {
        $name = $_.Name
        $termSet = $group.TermSets[$name]
        if ($termSet -eq $null) {
            $termSet = $group.CreateTermSet($name)
        }
        $termSets += @{$name=$termSet}
        $_.ChoiceList.Choice | % {
            $termValue = $_.Replace("&", "")
            $term = $termSet.Terms[$termValue]
            if ($term -eq $null) {
                Write-Host "Adding $termValue"
                $termSet.CreateTerm($termValue, 1033) | Out-Null
            }
        }
        $group.TermStore.CommitAll()        
    }
    $termSets
}
function Set-SPUserProfileProperties($termSets) {
    $sa = Get-SPServiceApplication | ?{$_.TypeName -eq "User Profile Service Application"}
    $context = [Microsoft.SharePoint.SPServiceContext]::GetContext($sa.ServiceApplicationProxyGroup, [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default)
    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager $context
    $properties = $upm.ProfilePropertyManager.GetCoreProperties()
    foreach ($key in $termSets.Keys) {
        $prop = $properties.GetPropertyByName($key)
        $prop.TermSet = $termSets[$key]
        $prop.Commit()
    }
}
function Set-SPUserProfileValues($termSets, [xml]$data) {
    $sa = Get-SPServiceApplication | ?{$_.TypeName -eq "User Profile Service Application"}
    $context = [Microsoft.SharePoint.SPServiceContext]::GetContext($sa.ServiceApplicationProxyGroup, [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default)
    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager $context
    
    $data.ProfileData.Users.User | % {
        if ($upm.UserExists($_.Account)) {
            $up = $upm.GetUserProfile($_.Account)
            Write-Host "Evaluating $($_.Account)..."
            foreach ($key in $termSets.Keys) {
                Write-Host "    Setting $key..."
                $prop = $up[$key]
                $prop.Clear()
                [string[]]$term = $_.GetAttribute($key).Split(';')
                $term | % {
                    if (![string]::IsNullOrEmpty($_)) {
                        $prop.Add($termSets[$key].Terms[$_].Name)
                    }
                }
            }
            $up.Commit()
        }
    }
}

$ts = New-Object Microsoft.SharePoint.Taxonomy.TaxonomySession (Get-SPSite "http://<ENTER YOUR SITE URL HERE>"),$true
[xml]$xml = Get-Content C:\userdata.xml
$termSets = Create-SPProfileTermSets $ts "Profile Properties" $xml
Set-SPUserProfileProperties $termSets
Set-SPUserProfileValues $termSets $xml

One note about the Taxonomy Session – notice that I don’t use the Get-SPTaxonomySession cmdlet – this is because I can’t stand how this cmdlet works; when you create a TaxonomySession object using the API you have the ability to force a reload from the database, thereby ignoring locally cached data, but when you use the cmdlet you don’t have this option. This can cause all kinds of issues when you are running scripts/functions like this repeatedly as what is cached may not reflect what is current and this will inevitably cause you headaches. (If you decide to use this code don’t forget to set the URL for the Taxonomy Session).

So that was my adventure for the day – as I said in the beginning, if there’s a way to do the upgrade that negates the need to do any of this please let me know; otherwise, perhaps others will benefit from today’s upgrade headaches :) .

18Apr/1132

Deploying SharePoint 2010 Solution Package Using PowerShell (Revisited)

If you were at my PowerShell for developers talk at the European SharePoint Best Practices Conference last week then you’ll know that I’ve never been all that happy with how I was approaching Farm Solution deployment, as detailed in an earlier post from sometime last year (Deploying SharePoint 2010 Solution Packages Using PowerShell). So what are some of the issues I have with what I created? Here’s a quick list:

  • There are two functions – I really just want one function to call and let the function figure out what to do based on the parameters provided.
  • I want to be able to provide a directory containing WSP files to deploy (sure, I could use Get-ChildItem to grab all my files, iterate through them, and then call the function, but that means I have to type more each time I want to execute and I’m way too lazy for that).
  • There’s no consideration for simply updating Solution Packages rather than retracting and redeploying.
  • I was using the Start-SPAdminJob cmdlet and stopping and starting the admin service – something that we shouldn’t be doing and is really just an old throwback to 2007. It’s just a bad idea – don’t do it.
  • I was forcing information such as GAC and CAS settings in the XML when I could easily get the information via the SPSolution object once added.
  • And finally, there was no real help available so you had to really know what was going on to understand how to construct the XML file and to then call the file.

For all these reasons I’ve decided to completely rewrite the script. As a result it’s a bit more complicated at first blush but that’s mainly due to some additional error handling, progress reporting, and blocking code that I’ve added; as well as the additional parameter related code and associated help. I’ve essentially followed the pattern that I described with my earlier post on Feature activation and have made the function work more like a cmdlet (with full help, parameter sets, and use of PipeBind objects). Before I share the code, I’d like to show the complete help that is available for the function:

NAME
    Deploy-SPSolutions
    
SYNOPSIS
    Deploys one or more Farm Solution Packages to the Farm.
    
SYNTAX
    Deploy-SPSolutions [-Identity] <String> [[-UpgradeExisting]] [[-AllWebApplications]] [[-WebApplication] <SPWebApplicationPipeBind[]>] [<CommonParameters>]
    
    Deploy-SPSolutions [-Config] <XmlDocument> [<CommonParameters>]
    
DESCRIPTION
    Specify either a directory containing WSP files, a single WSP file, or an XML configuration file containing the WSP files to deploy.
    If using an XML configuration file, the format of the file must match the following:
      <Solutions>    
        <Solution Path="<full path and filename to WSP>" UpgradeExisting="false">
          <WebApplications>            
         <WebApplication>http://example.com/</WebApplication>        
          </WebApplications>    
        </Solution>
      </Solutions>
    Multiple <Solution> and <WebApplication> nodes can be added. The UpgradeExisting attribute is optional and should be specified if the WSP should be udpated and not retracted and redeployed.

PARAMETERS
    -Config <XmlDocument>
        The XML configuration file containing the WSP files to deploy.
        
        Required?                    true
        Position?                    1
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -Identity <String>
        The directory, WSP file, or XML configuration file containing the WSP files to deploy.
        
        Required?                    true
        Position?                    1
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -UpgradeExisting [<SwitchParameter>]
        If specified, the WSP file(s) will be updated and not retracted and redeployed (if the WSP does not exist in the Farm then this parameter has no effect).
        
        Required?                    false
        Position?                    2
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -AllWebApplications [<SwitchParameter>]
        If specified, the WSP file(s) will be deployed to all Web Applications in the Farm (if applicable).
        
        Required?                    false
        Position?                    3
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -WebApplication <SPWebApplicationPipeBind[]>
        Specifies the Web Application(s) to deploy the WSP file to.
        
        Required?                    false
        Position?                    4
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".
    
INPUTS
    
OUTPUTS
    -------------------------- EXAMPLE 1 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo Web Application (if applicable).
    
    -------------------------- EXAMPLE 2 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo,http://mysites
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo and http://mysites Web Applications (if applicable).
    
    -------------------------- EXAMPLE 3 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -AllWebApplications
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to all Web Applications (if applicable).
    
    -------------------------- EXAMPLE 4 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications
    
    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable).
    
    -------------------------- EXAMPLE 5 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications -UpgradeExisting
    
    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable); existing deployments will be upgraded and not retracted and redeployed.
    
    -------------------------- EXAMPLE 6 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions C:\Solutions.xml
    
    This example loads the function into memory and then deploys all the WSP files specified by the Solutions.xml configuration file.
    
RELATED LINKS
    Get-Content
    Get-SPSolution
    Add-SPSolution
    Install-SPSolution
    Update-SPSolution
    Uninstall-SPSolution
    Remove-SPSolution 

As you can see, this is a lot more useful for someone wishing to execute this script as not only does it provide information about the XML structure but it also provides several usage examples.

So, without further delay, here’s the new version of the deployment script (note that I changed the function name to Deploy-SPSolutions so it won’t impact environments that depend on the old function):

function global:Deploy-SPSolutions() {
  <#
  .Synopsis
    Deploys one or more Farm Solution Packages to the Farm.
  .Description
    Specify either a directory containing WSP files, a single WSP file, or an XML configuration file containing the WSP files to deploy.
    If using an XML configuration file, the format of the file must match the following:
      <Solutions>    
        <Solution Path="<full path and filename to WSP>" UpgradeExisting="false">
          <WebApplications>            
            <WebApplication>http://example.com/</WebApplication>        
          </WebApplications>    
        </Solution>
      </Solutions>
    Multiple <Solution> and <WebApplication> nodes can be added. The UpgradeExisting attribute is optional and should be specified if the WSP should be udpated and not retracted and redeployed.
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo Web Application (if applicable).
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo,http://mysites
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo and http://mysites Web Applications (if applicable).
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -AllWebApplications
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to all Web Applications (if applicable).
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications
    
    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable).
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications -UpgradeExisting
    
    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable); existing deployments will be upgraded and not retracted and redeployed.
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions C:\Solutions.xml
    
    This example loads the function into memory and then deploys all the WSP files specified by the Solutions.xml configuration file.
  .Parameter Config
    The XML configuration file containing the WSP files to deploy.
  .Parameter Identity
    The directory, WSP file, or XML configuration file containing the WSP files to deploy.
  .Parameter UpgradeExisting
    If specified, the WSP file(s) will be updated and not retracted and redeployed (if the WSP does not exist in the Farm then this parameter has no effect).
  .Parameter AllWebApplications
    If specified, the WSP file(s) will be deployed to all Web Applications in the Farm (if applicable).
  .Parameter WebApplication
    Specifies the Web Application(s) to deploy the WSP file to.
  .Link
    Get-Content
    Get-SPSolution
    Add-SPSolution
    Install-SPSolution
    Update-SPSolution
    Uninstall-SPSolution
    Remove-SPSolution
  #>
  [CmdletBinding(DefaultParameterSetName="FileOrDirectory")]
  param (
    [Parameter(Mandatory=$true, Position=0, ParameterSetName="Xml")]
    [ValidateNotNullOrEmpty()]
    [xml]$Config,

    [Parameter(Mandatory=$true, Position=0, ParameterSetName="FileOrDirectory")]
    [ValidateNotNullOrEmpty()]
    [string]$Identity,
    
    [Parameter(Mandatory=$false, Position=1, ParameterSetName="FileOrDirectory")]
    [switch]$UpgradeExisting,
    
    [Parameter(Mandatory=$false, Position=2, ParameterSetName="FileOrDirectory")]
    [switch]$AllWebApplications,
    
    [Parameter(Mandatory=$false, Position=3, ParameterSetName="FileOrDirectory")]
    [Microsoft.SharePoint.PowerShell.SPWebApplicationPipeBind[]]$WebApplication
  )
  function Block-SPDeployment($solution, [bool]$deploying, [string]$status, [int]$percentComplete) {
    do { 
      Start-Sleep 2
      Write-Progress -Activity "Deploying solution $($solution.Name)" -Status $status -PercentComplete $percentComplete
      $solution = Get-SPSolution $solution
      if ($solution.LastOperationResult -like "*Failed*") { throw "An error occurred during the solution retraction, deployment, or update." }
      if (!$solution.JobExists -and (($deploying -and $solution.Deployed) -or (!$deploying -and !$solution.Deployed))) { break }
    } while ($true)
    sleep 5  
  }
  switch ($PsCmdlet.ParameterSetName) { 
    "Xml" { 
      # An XML document was provided so iterate through all the defined solutions and call the other parameter set version of the function
      $Config.Solutions.Solution | ForEach-Object {
        [string]$path = $_.Path
        [bool]$upgrade = $false
        if (![string]::IsNullOrEmpty($_.UpgradeExisting)) {
          $upgrade = [bool]::Parse($_.UpgradeExisting)
        }
        $webApps = $_.WebApplications.WebApplication
        Deploy-SPSolutions -Identity $path -UpgradeExisting:$upgrade -WebApplication $webApps -AllWebApplications:$(($webApps -eq $null) -or ($webApps.Length -eq 0))
      }
      break
    }
    "FileOrDirectory" {
      $item = Get-Item (Resolve-Path $Identity)
      if ($item -is [System.IO.DirectoryInfo]) {
        # A directory was provided so iterate through all files in the directory and deploy if the file is a WSP (based on the extension)
        Get-ChildItem $item | ForEach-Object {
          if ($_.Name.ToLower().EndsWith(".wsp")) {
            Deploy-SPSolutions -Identity $_.FullName -UpgradeExisting:$UpgradeExisting -WebApplication $WebApplication
          }
        }
      } elseif ($item -is [System.IO.FileInfo]) {
        # A specific file was provided so assume that the file is a WSP if it does not have an XML extension.
        [string]$name = $item.Name
        
        if ($name.ToLower().EndsWith(".xml")) {
          Deploy-SPSolutions -Config ([xml](Get-Content $item.FullName))
          return
        }
        $solution = Get-SPSolution $name -ErrorAction SilentlyContinue
        
        if ($solution -ne $null -and $UpgradeExisting) {
          # Just update the solution, don't retract and redeploy.
          Write-Progress -Activity "Deploying solution $name" -Status "Updating $name" -PercentComplete -1
          $solution | Update-SPSolution -CASPolicies:$($solution.ContainsCasPolicy) `
            -GACDeployment:$($solution.ContainsGlobalAssembly) `
            -LiteralPath $item.FullName
        
          Block-SPDeployment $solution $true "Updating $name" -1
          Write-Progress -Activity "Deploying solution $name" -Status "Updated" -Completed
          
          return
        }

        if ($solution -ne $null) {
          #Retract the solution
          if ($solution.Deployed) {
            Write-Progress -Activity "Deploying solution $name" -Status "Retracting $name" -PercentComplete 0
            if ($solution.ContainsWebApplicationResource) {
              $solution | Uninstall-SPSolution -AllWebApplications -Confirm:$false
            } else {
              $solution | Uninstall-SPSolution -Confirm:$false
            }
            #Block until we're sure the solution is no longer deployed.
            Block-SPDeployment $solution $false "Retracting $name" 12
            Write-Progress -Activity "Deploying solution $name" -Status "Solution retracted" -PercentComplete 25
          }

          #Delete the solution
          Write-Progress -Activity "Deploying solution $name" -Status "Removing $name" -PercentComplete 30
          Get-SPSolution $name | Remove-SPSolution -Confirm:$false
          Write-Progress -Activity "Deploying solution $name" -Status "Solution removed" -PercentComplete 50
        }

        #Add the solution
        Write-Progress -Activity "Deploying solution $name" -Status "Adding $name" -PercentComplete 50
        $solution = Add-SPSolution $item.FullName
        Write-Progress -Activity "Deploying solution $name" -Status "Solution added" -PercentComplete 75

        #Deploy the solution
        
        if (!$solution.ContainsWebApplicationResource) {
          Write-Progress -Activity "Deploying solution $name" -Status "Installing $name" -PercentComplete 75
          $solution | Install-SPSolution -GACDeployment:$($solution.ContainsGlobalAssembly) -CASPolicies:$($solution.ContainsCasPolicy) -Confirm:$false
          Block-SPDeployment $solution $true "Installing $name" 85
        } else {
          if ($WebApplication -eq $null -or $WebApplication.Length -eq 0) {
            Write-Progress -Activity "Deploying solution $name" -Status "Installing $name to all Web Applications" -PercentComplete 75
            $solution | Install-SPSolution -GACDeployment:$($solution.ContainsGlobalAssembly) -CASPolicies:$($solution.ContainsCasPolicy) -AllWebApplications -Confirm:$false
            Block-SPDeployment $solution $true "Installing $name to all Web Applications" 85
          } else {
            $WebApplication | ForEach-Object {
              $webApp = $_.Read()
              Write-Progress -Activity "Deploying solution $name" -Status "Installing $name to $($webApp.Url)" -PercentComplete 75
              $solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -WebApplication $webApp -Confirm:$false
              Block-SPDeployment $solution $true "Installing $name to $($webApp.Url)" 85
            }
          }
        }
        Write-Progress -Activity "Deploying solution $name" -Status "Deployed" -Completed
      }
      break 
    }
  } 
}

When it comes to using the function I believe the help documentation speaks for itself so I won’t reiterate it here.

As always, I’m open to suggestions as to how to improve this function so please leave a comment if you find something wrong or have a suggestion for making it better.

-Gary