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

25Jun/1128

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.

18Jun/116

“Automating SharePoint 2010 with Windows PowerShell 2.0” Now Available (and the Bruins win the Cup!)

Boston Bruins - 2011 Stanley Cup Champions!This week was a particularly good week – one that I’ve been waiting to have for a *very* long time. Many who know me know that I’m a huge Boston Bruins fan – I grew up in New Hampshire and have bled black and gold my whole life (even  during those late ‘90s and early ‘00 years when many were ashamed to admit they were Bruins fans); well this past Wednesday, after 39 years, the Bruins finally brought Lord Stanley’s Cup home to Boston! For those that don’t follow hockey this probably doesn’t mean much – but to a true hockey fan this is way greater than the Super Bowl or the World Cup or anything like that – and for someone from Boston, the first US based NHL team, it’s that much greater. So with that win my week was looking pretty good – and then Thursday came and, still high from the win, I finally got my own physical copy of the book which I (and my co-author, Shannon Bray) spent so much time working on over the last year. And that brings me to the real purpose of this post – to officially announce the release of the book!

The book actually released earlier last week but due to a minor miscommunication I didn’t get my author copy of the book until just this Thursday – and I didn’t want to put this together until I actually had the thing in my hands. Before I say anything else I want to first thank my co-author, Shannon Bray – I can get pretty nit-picky with technical stuff like this and Shannon was great in how receptive he was to any feedback I had with his chapters. I also want to thank Spencer Harbar for helping with the technical reviews of the book – Spence wasn’t able to review everything we wrote due to the way the edit process works with Sybex but I believe that the book is greatly improved due to his contributions (if there’s an error in the book you can assume that’s something Spence didn’t get to review Smile). And of course I need to thank the whole Sybex team for making the book possible.

Okay, enough of that – let’s talk about what’s in the book. Let me first provide you with some helpful links to where you can purchase the book or download the supporting materials and preview chapters and whatnot:

There’s also supposed to be a ton of sample scripts available for download but for some reason those haven’t made it up on the book’s site yet (I’ve got an inquiry into Sybex to see what’s up – as soon as they are posted I’ll send out a tweet with the download link – follow me on twitter for updates: @glapointe). Also, from the Amazon site you can use the “Look Inside” feature (just click the book logo) to view some additional content as well as the first few pages of most chapters.

One note about Appendix A – this particular chapter is actually my favorite chapter – it was the most fun to write and includes a *ton* of information that is not documented anywhere. When Shannon and I put the original chapter outline together we did not have this chapter included as the original target audience was supposed to be IT administrators – as a result, Sybex did not budget for the additional page count and the chapter got dropped (I was upset that it got labeled as an Appendix and then more upset that it got dropped, but Sybex is running a business and I respect their decision, I’m just bummed that I couldn’t see it in print).

Also, those of you that pay close attention to details might note that the book title changed slightly (I didn’t know this until I got it in my hands). The book was originally “Automating SharePoint 2010 with Windows PowerShell 2.0” but it got changed to “Automating SharePoint 2010 Administration with Windows PowerShell 2.0” (I know, it’s verbose). It’s a subtle change meant to bring the title in line with other books that Sybex is releasing. However, what I want to be clear about is that this book is not just meant for IT administrators – true, they will benefit from it the most, but remember, I’m a developer, that’s where my passion is, and as such I tried to include as many nuggets as I could to make this book a value to both IT administrators and developers (heck, just the first three chapters on Windows PowerShell in general should be a must read for both audiences).

Now let’s take a look at what is in the book. I’ve pasted below the chapter details as found in the front section of the book; one thing I added is who was responsible for each chapter. I should note that, in order to help enforce a sense of consistency and technical accuracy, I did provide Shannon with a lot of assistance on his chapters, so though I’ve only called out the chapters that I was primarily responsible for, I was in fact a major contributor on all chapters.

Part 1 - Getting Started With Windows PowerShell Basics

  • Chapter 1, “Windows PowerShell 101,” begins with a basic understanding of Windows PowerShell: the shell, cmdlets, variables, and types and how to work with and output data. (Gary)
  • Chapter 2, “Filtering and Iterating Your Data,” expands on the concepts of Chapter 1 by showing how to add structure and logic to commands. (Gary)
  • Chapter 3, “Making Your PowerShell Reusable,” completes the basic Windows PowerShell run-through by explaining how to turn a series of commands into functions, scripts, and modules. (Gary)

Part 2 - Installing and Configuring a SharePoint 2010 Environment

  • Chapter 4, “Deploying New Installations and Upgrades,” details how to create an installation script for new Farm installations and presents upgrade scenarios and scripts that can ease the upgrade process. (Gary)
  • Chapter 5, “Configuring Server Communications,” builds on the information learned in Chapter 4 by detailing the various settings that affect the communication of SharePoint with and through dependent resources, such as network adapters, the firewall, and Active Directory. (Shannon)
  • Chapter 6, “Configuring Farm Application Settings,” shows how to use Windows PowerShell to configure some of the core configuration options available on the General Application Settings page of the Central Administration site. (Shannon)

Part 3 - Deploying and Managing Applications

  • Chapter 7, “Managing Web Applications,” covers the creation and manipulation of Web Applications and the various configurable properties that are scoped to the Web Application level. (Shannon)
  • Chapter 8, “Managing Site Collections and Sites,” continues where Chapter 7 left off by covering the creation and manipulation of Site Collections and Sites and the various configurable properties that are scoped the Site Collection or Site level. (Shannon)
  • Chapter 9, “Understanding Authentication,” builds on the information from Chapter 7 by detailing the Classic and Claims authentication modes that must be taken into account when provisioning and working with Web Applications. (Shannon)
  • Chapter 10, “Managing Features and Solutions,” details how to manage SharePoint Solution Packages and Features, including those deployed to the Solutions Gallery (Sandbox Solutions). (Shannon)

Part 4 - Services and Service Applications

  • Chapter 11, “Managing Service Applications,” introduces the Service Application concepts and foundational information necessary to understand the subsequent chapters that focus on provisioning individual Service Applications. (Gary)
  • Chapter 12, “Provisioning Support Services,” covers the provisioning of the core Service Applications that are necessary for SharePoint and/or other Service Applications to function properly, including: Web Analytics Services, State Services, Secure Store Services, User Code Services, Claims to Windows Token Services, and the Usage and Health Data Collection Services. (Shannon)
  • Chapter 13, “Provisioning Business Intelligence, Business Connectivity, and Word Automation Services,” continues the Service Application provisioning with the Business Intelligence–focused Service Applications (Excel, Access, Visio, and PerformancePoint) and the Business Connectivity Services (BCS) and Word Automation Services Applications. (Gary)
  • Chapter 14, “Provisioning Search Services,” tackles what is undoubtedly the most complex Service Application, the Enterprise Search Services Service Application, and it concludes with the Foundation Search, the only place where STSADM will be used in the book. (Shannon)
  • Chapter 15, “Provisioning Metadata and User Profile Services,” completes the Service Application provisioning story with the Managed Metadata Services Service Application and the User Profile Services Service Application, including coverage of the many issues oft en experienced with the User Profile Synchronization Service. (Gary)

Part 5 - Managing and Maintaining a SharePoint Environment

  • Chapter 16, “Managing Operational Settings,” covers monitoring SharePoint, including how to work with Unified Logging Service (ULS) logs and the Health Analyzer, and Timer Jobs, and concludes with coverage of the Developer Dashboard. (Shannon)
  • Chapter 17, “Back Up and Restore a SharePoint Environment,” details the various backup and restore cmdlets available for backing up the Farm, Site Collections, Sites, and lists. (Shannon)
  • Chapter 18, “Optimizing the Performance of a SharePoint Environment,” walks through the most common areas where performance gains can be had, including resource throttling, caching, and remote binary large object (BLOB) storage. (Shannon)

Part 6 - Advanced Administration

  • Chapter 19, “Remote Administration,” shows how to connect remotely to and work with a SharePoint Farm using Windows PowerShell, thus reducing the need for direct server access. (Gary)
  • Chapter 20, “Multi-Tenancy,” builds on the previous chapters by demonstrating how to build a SharePoint hosting Farm. (Gary)
  • Appendix A, “Creating Custom Cmdlets,” takes the SharePoint PowerShell story to the next level by showing how to extend the
    out-of-the-box capabilities by adding new cmdlets, views, and extensions. (Gary)

The book came in at 737 pages - for my first attempt at writing a book this was rather immense (it will make a great doorstop some day Smile). Shannon and I did our best to make the book informative and technically accurate – hopefully readers of the book will agree – that said, if you find something you disagree with or that is simply wrong/inaccurate/incomplete/total bullsh**/whatever, please let us know (just please be kind Smile). You can either fill out the errata form on the Sybex site or post a comment here (there were many, *many* late nights and weekends involved with the writing of this book and, as such, it’s quite possible that I brain farted a thing or two).

Shannon and I hope you enjoy the book – if you do, please tell a friend, if you don’t, well, um, the book’s already printed so not much I can do to fix it but maybe I can do some more blogging to make up for it Smile.

-Gary

Tagged as: 6 Comments
7May/110

Monitor SharePoint User Profile Changes

Today I got my copy of the May 2011 edition of SharePoint Pro magazine and was pleased to find the “Monitor SharePoint User Profile Changes” article that I co-wrote with my buddy Matthew McDermott! The cool thing for me is that this is my first ever magazine article so I’m very excited! You can find the article online at the SharePoint Pro site: http://www.sharepointpromag.com/article/sharepoint/monitor-sharepoint-user-profile-changes-129846.

Note that for some reason the online version of the article only shows my picture for the authors and you have to look closely to see Matt’s name which makes it look, at first glance, like there was just one author, me – I’m really bummed about this for if it wasn’t for Matt I wouldn’t even have gotten the opportunity to write this article – fortunately, the print version gives better props to both of us; so, Matt, if you’re reading this, thanks for helping me to achieve this long standing goal!

-Gary

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/1127

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

17Apr/111

Retrieving SharePoint 2010 Feature Activations Using Windows PowerShell

During my PowerShell for Developers presentation in London last week I promised to show and demonstrate a script for retrieving Feature activations; unfortunately I ran out of time and was not able to show this script to the degree that I’d intended so I decided to throw together this blog post.

When developing custom Features it is very common to expect that there will need to be some level of update required for those Features. Typically this means that, after deploying the Feature via a Solution Package, you will need to re-activate that Feature in order to trigger any additional code to run (or, if you are using the new SharePoint 2010 Feature upgrade capabilities you will need to run the Upgrade(Boolean) method of the SPFeature object). The problem is knowing where the Feature is activated throughout the Farm. Using PowerShell there are two ways to do this – you can use the Get-SPFeature cmdlet and test the results against the appropriate scope or you can use the various “Query” methods that have been provided for each scope. I don’t recommend that you use the Get-SPFeature cmdlet as it is very inefficient, and as such, I won’t bother showing an example of that here. Instead I’ll focus on the “Query” methods approach.

Whether your Feature is scoped to the Farm, Web Application, Site Collection, or Site, there is a method that you can call to get an SPFeature object which effectively corresponds to a Feature activation. For Farm scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPWebService class, obtainable via the SPWebService class’ static AdministrationService property; for Web Application scoped Features you use the static QueryFeaturesInAllWebServices(Guid, Boolean) method of the SPWebService class; for Site Collection scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPWebApplication class; and for Site scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPSite class.

To create our PowerShell function we’ll simply take in a SPFeatureDefinition object and use a switch statement to call the appropriate method based on the scope of the Feature. To make the function more versatile we can use the Microsoft.SharePoint.PowerShell.SPFeatureDefinitionPipeBind type which will allow the caller to pass in either the name of the Feature, its ID, or an actual SPFeatureDefinition object; additionally, we can use parameter attributes to easily allow the value to be passed in via the object pipeline. And finally, we’ll add an additional parameter stating that we wish to retrieve only those activations that require upgrading and we’ll add some basic help for the function.

The following code listing represents the completed function – I recommend that you save this to a file named Get-SPFeatureActivations.ps1. Note that I plan on adding this as a cmdlet to my downloadable extensions thereby making the need for this script unnecessary, however, I believe that this example provides a great template to use for creating professional looking, production ready scripts that both IT administrators and developers can use.

function Get-SPFeatureActivations() {
  <#
  .Synopsis
    Retrieves Feature activations for the given Feature Definition.
  .Description
    Retrieves the SPFeature object for each activation of the SPFeatureDefinition object.
  .Example
    Get-SPFeatureActivations TeamCollab
  .Parameter Identity
    The Feature name, ID, or SPFeatureDefinition object whose activations will be retrieved.
  .Parameter NeedsUpgrade
    If specified, only Feature activations needing upgrading will be retrieved.
  .Link
    Get-SPFeature
  #>
  [CmdletBinding()]
  param (
    [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
    [Alias("Feature")]
    [ValidateNotNullOrEmpty()]
    [Microsoft.SharePoint.PowerShell.SPFeatureDefinitionPipeBind]$Identity,
    
    [Parameter(Mandatory=$false, Position=1)]
    [switch]$NeedsUpgrade
  )
  begin { }
  process {
    $fd = $Identity.Read()
    switch ($fd.Scope) {
      "Farm" {
        [Microsoft.SharePoint.Administration.SPWebService]::AdministrationService.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent)
        break
      }
      "WebApplication" {
        [Microsoft.SharePoint.Administration.SPWebService]::QueryFeaturesInAllWebServices($fd.ID, $NeedsUpgrade.IsPresent)
        break
      }
      "Site" {
        foreach ($webApp in Get-SPWebApplication) {
          $webApp.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent)
        }
        break
      }
      "Web" {
        foreach ($site in Get-SPSite -Limit All) {
          $site.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent)
          $site.Dispose()
        }
        break
      }
    }
  }
  end { }
}

Assuming you’ve saved the file to the root of the C drive (not recommended but its what I do when I’m doing demos) then you can load the function into memory using dot sourcing as shown in the following example (note that the help for the function shows the help information specified by the block comment help):

SNAGHTML45555695

Once the function is loaded into memory you can start using it. In the following example I’m returning back all the locations where the MyCustomFeature Feature is activated; I then use the Select-Object cmdlet to return just the URL for each activation:

Get-SPFeatureActivations MyCustomFeature | select @{Expression={$_.Parent.Url};Label="Url"}

In this next example, instead of simply outputting the URL of each activation, I’m forcing the Feature to be reactivated using the Enable-SPFeature cmdlet (use the -Force parameter to force the Feature to be reactivated – you could also change the code to deactivate the Feature using the Disable-SPFeature cmdlet and then activate using the Enable-SPFeature cmdlet):

Get-SPFeatureActivations MyCustomFeature | ForEach-Object {
  Enable-SPFeature -Identity MyCustomFeature -Url $_.Parent.Url -Force

}

Similarly you can retrieve only those Features needing upgrade and then call the Upgrade() method, as shown in this next example:

Get-SPFeatureActivations MyCustomFeature -NeedsUpgrade | ForEach-Object {
  $_.Upgrade($false
)
}

I strongly recommend that, before you re-deploy a Feature that may be activated at an unknown number of scopes, you run this function (or something similar to it) so that you fully understand the impact of upgrading your Feature. One more thing to watch out for, if your environment is very large you may wish to modify this function so that it does not return the SPFeature object but instead just returns the URL corresponding to the activation – you can then use the Get-SPFeature cmdlet to retrieve the SPFeature object; the benefit of this is that you can immediately dispose of the parent object and prevent potential out of memory errors (I’m particularly concerned with Site Collection and Site scoped Features here where the Parent property of the SPFeature object corresponds to an SPSite or SPWeb object which must be disposed).

That’s all I’ve got for now; hopefully you’ve found this useful!

-Gary

17Apr/111

European SharePoint Best Practices Conference Wrap Up

I just got back from London and all I can say is, “Wow!” This was the first time that myself and my wife and daughter have ever been out of the US and we had an absolute blast – we did so much walking that we literally wore our shoes thin – it’s truly an amazing place with incredible history everywhere you turn.

As for the conference itself, first off I want to thank Steve Smith and all those that were involved with organizing such a wonderful conference – as a speaker they definitely set the bar for other conference organizers and I hope that attendees saw the same attention to detail and overall quality that the speakers saw (this conference is truly unique amongst all the ones I’ve spoken at).

In regards to my two sessions – I totally blew my timing on both of them (more so on the developer one) and was unable to show everything that I planned but I do hope that those that attended got some useful information for the time spent (virtually nobody left when I ran late so I’m going to take that as a good sign that people were getting value out of the presentations). I plan to post some of the sample scripts that I demonstrated during the presentations but that will come over the next few weeks; for now I’ve posted my slide decks (saved with notes so you can see some of my examples) for you to download:

Windows PowerShell for SharePoint 2010 Administrators Windows PowerShell for SharePoint 2010 Administrators
Windows PowerShell for SharePoint 2010 Developers Windows PowerShell for SharePoint 2010 Developers

Don’t forget that you can also download the PowerShell cheat sheet that I provided during the sessions (see my earlier post).

Thanks again to everyone that attended my sessions and for all the great tweets that came out during and after the sessions – I know I still have lots to learn when it comes to public speaking so any kind of feedback is very much appreciated!

17Apr/111

Windows PowerShell Cheat Sheet

While preparing for my two PowerShell talks that I presented at the European SharePoint Best Practices Conference last week in London, I soon discovered that I had way too much content to present. Specifically, I wanted to begin both talks with a “PowerShell 101” piece where I walked through some of the more basic concepts relevant to each audience (mainly those that I always got tripped up on); unfortunately there’s just too many things to choose from and I couldn’t possibly show everything I wanted. Well, I happened to be in Orlando the week before I left for the conference and I had a chance to talk with Rob Bogue about what my plans were – well, to make a long story short, Rob had stated that the one thing he’d love to have is a simple, one-page cheat sheet showing some common syntactical examples. I thought this was not only a great idea in general but could also help solve my problem for my presentations – now I didn’t have to show everything during my talk, I could just show those bits that warranted more explanation and then provide everyone the cheat sheet as a handout for later review. And of course, I never planned on limiting the audience to just the attendees of the conference, though I did want them to be the first recipients. So, feel free to download my Windows PowerShell Cheat Sheet and good luck with all your PowerShell endeavors!

Tagged as: 1 Comment
21Jan/112

Creating PowerShell Help Files Dynamically

It’s been a while since I’ve blogged anything useful due to all my writing efforts going to the book that I’ve been working on. However, I’ve recently wrapped up my last chapter for the book so I figured what better time to start focusing on my blog again.

The first thing I wanted to cover was something that I should have documented back in May of 2010 when I released my SharePoint 2010 cmdlets and it is something that I just finished referencing in my last book chapter: creating help files for custom cmdlets.

In the book I emphasize that creating these help files is not a trivial task and there are virtually no good tools available to help you with the effort. When I created my SharePoint 2007 cmdlets, I had first tried using the Cmdlet Help Editor v1.0 that the Windows PowerShell team released back in May of 2007; I quickly found that this wasn’t very practical for long term use for numerous reasons, particularly when looking at SharePoint cmdlets where I don’t have a snap-in that I can point the tool to. But the biggest issue I had with using this tool, or any other tool that required me to add descriptions manually, was that I just couldn’t keep up with the changes. I was making new cmdlets and extending existing ones so fast that it was taking me longer to update the help file than it was to create/update the cmdlet – that was just unacceptable to me.

As you may know, I’m all about automation; so I figured, how hard could it possibly be to automate the creation of the help file? All I needed to do was to decorate my cmdlets with some metadata that I could then use with a little reflection to spit out the required XML. I could then generate this XML on the Post Build event of my assembly so that the help file would be automatically updated just prior to the SharePoint Solution Package (WSP) being generated. So, back during the beta days of SharePoint 2010, while migrating my 2007 STSADM extensions to cmdlets, I decided to go ahead and implement my idea. I released it along with my SharePoint 2010 source code with little fanfare (not very many people are generating PowerShell cmdlets and even fewer are creating help files for them). However, with my book having a reference to this code I figured I would write a short post about it and, along the way, break the code out of the SharePoint project and into it’s own project, thereby making it useful for anyone who wishes to dynamically develop Windows PowerShell help files for their cmdlets.

Downloads
You can download the .NET Assembly (Lapointe.PowerShell.MamlGenerator.dll) alone or the source code to the assembly from my downloads page. I thought about providing a separate download for the source but I’m lazy and really didn’t want to have to manage two different download packages; so, if you wish to download the source, just download the SharePoint 2010 source code and you’ll find the Lapointe.PowerShell.MamlGenerator project.

So how does it work? It’s actually quite simple; I’ve created a series of custom Attribute classes which you can use to decorate your cmdlet class. I then use reflection to interrogate the Assembly for any classes that are based on PSCmdlet; for each class I look for these attributes. The rest is just simple XML generation. The Attributes are defined in the following table:

Name Example

CmdletDescriptionAttribute

This Attribute is assigned to the class and contains the main, verbose description, as well as the synopsis for the cmdlet (or shorter description). You can only have one CmdletDescriptionAttribute assigned to the class.

[CmdletDescription("Delete a list from a web site.")]

ExampleAttribute

This Attribute is assigned to the class and contains any example code and corresponding descriptions. You may assign multiple instances of the ExampleAttribute to the class (one for each example).

[Example(
    Code = "PS C:\\> Get-SPList \"http://server_name/lists/mylist\" | Remove-SPList -BackupDirectory \"c:\\backups\\mylist\"", 
    Remarks = "This example deletes the list mylist and creates a backup of the list in the c:\\backups\\mylist folder.")]

RelatedCmdletsAttribute

This Attribute is assigned to the class and contains the listing of related cmdlets. The related cmdlets can be added using the cmdlet's type or any applicable string representation of the cmdlet (appropriate for related cmdlets that are external to the current project). You can only have one RelatedCmdletsAttribute assigned to the class.

[RelatedCmdlets(
    typeof(SPCmdletGetList),
    ExternalCmdlets = new[] {"Get-SPWeb"})]

SupportsWildcardsAttribute

This Attribute is assigned to the class and indicates whether the cmdlet supports wildcards. This Attribute is a marker only and contains no properties. You can only have one SupportsWildcardsAttribute assigned to the class.

[SupportsWildcards]

These are all the custom Attributes that I’m using; the rest are are native to PowerShell and can be found in the System.Management.Automation namespace. Primarily I’m using CmdletAttribute and ParameterAttribute.

The example is taken from my SharePoint 2010 cmdlets and demonstrates how most of these Attributes are used:

using System.Collections.Generic;
using Microsoft.SharePoint;
using Microsoft.SharePoint.PowerShell;
using System.Management.Automation;
using Lapointe.SharePoint2010.Automation.Cmdlets.PipeBindObjects;
using Lapointe.PowerShell.MamlGenerator.Attributes;

namespace Lapointe.SharePoint2010.Automation.Cmdlets.Lists
{
    [Cmdlet(VerbsCommon.Get, "SPList", SupportsShouldProcess = false),
    SPCmdlet(RequireLocalFarmExist = true)]
    [CmdletDescription("Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.")]
    [RelatedCmdlets(typeof(SPCmdletDeleteList), typeof(SPCmdletCopyList), typeof(SPCmdletCopyListSecurity),
        typeof(SPCmdletExportListSecurity), ExternalCmdlets = new[] {"Get-SPWeb", "Start-SPAssignment", "Stop-SPAssignment"})]
    [Example(Code = "PS C:\\> $list = Get-SPList \"http://server_name/lists/mylist\"",
        Remarks = "This example retrieves the list at http://server_name/lists/mylist.")]
    public class SPCmdletGetList : SPGetCmdletBaseCustom<SPList>
    {
        #region Parameters

        [Parameter(Mandatory = false,
            ValueFromPipeline = true,
            Position = 0,
            ParameterSetName = "AllListsInIdentity")]
        public SPListPipeBind Identity { get; set; }

        [Parameter(Mandatory = false,
            ValueFromPipeline = false,
            ParameterSetName = "AllListsByType")]
        public SPBaseType ListType { get; set; }

        [Parameter(Mandatory = false,
            ValueFromPipeline = true,
            HelpMessage = "Specifies the URL or GUID of the Web containing the list to be retrieved.\r\n\r\nThe 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.")]
        public SPWebPipeBind Web { get; set; }

        #endregion

        protected override IEnumerable<SPList> RetrieveDataObjects()
        {
            List<SPList> lists = new List<SPList>();
            SPWeb web = null;
            if (this.Web != null)
                web = this.Web.Read();

            if (Identity == null && ParameterSetName != "AllListsByType")
            {
                foreach (SPList list in web.Lists)
                    lists.Add(list);
            }
            else if (Identity == null && ParameterSetName == "AllListsByType")
            {
                foreach (SPList list in web.GetListsOfType(ListType))
                    lists.Add(list);
            }
            else
            {
                SPList list = this.Identity.Read(web);
                if (list != null)
                    lists.Add(list);
            }

            AssignmentCollection.Add(web);
            foreach (SPList list1 in lists)
            {
                AssignmentCollection.Add(list1.ParentWeb);
                AssignmentCollection.Add(list1.ParentWeb.Site);
            }

            return lists;
        }
    }
}

With the cmdlet properly decorated I can make quick changes to my code and update the help documentation as I update the cmdlet. I don’t have to remember to go to another class file and remember this extremely cryptic MAML based XML format. This makes me considerably more efficient and it keeps my help files more up to date and inline with the actual cmdlet (I wish Microsoft would do something like this as much of the help for the SharePoint 2010 cmdlets is inaccurate, incomplete, or missing entirely).

The following shows a snippet of the help file that gets generated based on the attributes defined (I also use the Assembly’s CopyrightAttribute and DescriptionAttribute to add the copyright details that you see in the XML below):

<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/1" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10">
  <command:details>
    <command:name>Get-SPList</command:name>
    <maml:description>
      <maml:para>Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.</maml:para>
    </maml:description>
    <maml:copyright>
      <maml:para>Copyright 2010 Falchion Consulting, LLC</maml:para>
      <maml:para>  &gt; For more information on this cmdlet and others:</maml:para>
      <maml:para>  &gt; http://blog.falchionconsulting.com/</maml:para>
      <maml:para>  &gt; Use of this cmdlet is at your own risk.</maml:para>
      <maml:para>  &gt; Gary Lapointe assumes no liability.</maml:para>
    </maml:copyright>
    <command:verb>Get</command:verb>
    <command:noun>SPList</command:noun>
    <dev:version>1.0.0.0</dev:version>
  </command:details>
  <maml:description>
    <maml:para>Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.</maml:para>
    <maml:para />
    <maml:para>Copyright 2010 Falchion Consulting, LLC</maml:para>
    <maml:para>  &gt; For more information on this cmdlet and others:</maml:para>
    <maml:para>  &gt; http://blog.falchionconsulting.com/</maml:para>
    <maml:para>  &gt; Use of this cmdlet is at your own risk.</maml:para>
    <maml:para>  &gt; Gary Lapointe assumes no liability.</maml:para>
  </maml:description>
  <command:syntax>
    <command:syntaxItem>
      <maml:name>Get-SPList</maml:name>
      <command:parameter required="false" position="1">
        <maml:name>Identity</maml:name>
        <command:parameterValue required="true">SPListPipeBind</command:parameterValue>
      </command:parameter>
      <command:parameter required="false" position="named">
        <maml:name>Web</maml:name>
        <command:parameterValue required="true">SPWebPipeBind</command:parameterValue>
      </command:parameter>
      <command:parameter required="false" position="named">
        <maml:name>AssignmentCollection</maml:name>
        <command:parameterValue required="true">SPAssignmentCollection</command:parameterValue>
      </command:parameter>
    </command:syntaxItem>
    <command:syntaxItem>
      <maml:name>Get-SPList</maml:name>
      <command:parameter required="false" position="named">
        <maml:name>ListType</maml:name>
        <command:parameterValue required="true">GenericList | DocumentLibrary | Unused | DiscussionBoard | Survey | Issue | UnspecifiedBaseType</command:parameterValue>
      </command:parameter>
      <command:parameter required="false" position="named">
        <maml:name>Web</maml:name>
        <command:parameterValue required="true">SPWebPipeBind</command:parameterValue>
      </command:parameter>
      <command:parameter required="false" position="named">
        <maml:name>AssignmentCollection</maml:name>
        <command:parameterValue required="true">SPAssignmentCollection</command:parameterValue>
      </command:parameter>
    </command:syntaxItem>
  </command:syntax>
  <command:parameters>
    <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="1" variableLength="false">
      <maml:name>Identity</maml:name>
      <maml:description>
        <maml:para />
      </maml:description>
      <command:parameterValue required="false" variableLength="false">SPListPipeBind</command:parameterValue>
      <dev:type>
        <maml:name>SPListPipeBind</maml:name>
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:parameter>
    <command:parameter required="false" globbing="false" pipelineInput="false" position="named" variableLength="false">
      <maml:name>ListType</maml:name>
      <maml:description>
        <maml:para />
      </maml:description>
      <command:parameterValue required="false" variableLength="false">SPBaseType</command:parameterValue>
      <dev:type>
        <maml:name>SPBaseType</maml:name>
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:parameter>
    <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="named" variableLength="false">
      <maml:name>Web</maml:name>
      <maml:description>
        <maml:para>Specifies the URL or GUID of the Web containing the list to be retrieved.</maml:para>
        <maml:para />
        <maml:para>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.</maml:para>
      </maml:description>
      <command:parameterValue required="false" variableLength="false">SPWebPipeBind</command:parameterValue>
      <dev:type>
        <maml:name>SPWebPipeBind</maml:name>
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:parameter>
    <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="named" variableLength="false">
      <maml:name>AssignmentCollection</maml:name>
      <maml:description>
        <maml:para>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.</maml:para>
        <maml:para />
        <maml:para>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.</maml:para>
      </maml:description>
      <command:parameterValue required="false" variableLength="false">SPAssignmentCollection</command:parameterValue>
      <dev:type>
        <maml:name>SPAssignmentCollection</maml:name>
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:parameter>
  </command:parameters>
  <command:inputTypes>
    <command:inputType>
      <dev:type>
        <maml:name />
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:inputType>
  </command:inputTypes>
  <command:returnValues>
    <command:returnValue>
      <dev:type>
        <maml:name />
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:returnValue>
  </command:returnValues>
  <command:terminatingErrors />
  <command:nonTerminatingErrors />
  <maml:alertSet>
    <maml:title />
    <maml:alert>
      <maml:para>For more information, type "Get-Help Get-SPList -detailed". For technical information, type "Get-Help Get-SPList -full".</maml:para>
    </maml:alert>
  </maml:alertSet>
  <command:examples>
    <command:example>
      <maml:title>------------------EXAMPLE------------------</maml:title>
      <dev:code>PS C:\&gt; $list = Get-SPList "http://server_name/lists/mylist"</dev:code>
      <dev:remarks>
        <maml:para>This example retrieves the list at http://server_name/lists/mylist.</maml:para>
      </dev:remarks>
    </command:example>
  </command:examples>
  <maml:relatedLinks>
    <maml:navigationLink>
      <maml:linkText>Remove-SPList</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Copy-SPList</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Copy-SPListSecurity</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Export-SPListSecurity</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Get-SPWeb</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Start-SPAssignment</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Stop-SPAssignment</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
  </maml:relatedLinks>
</command:command>

As you can see, there is a lot of XML that you would have to manually generate (with over 50 cmdlets at the time of writing this and close to a hundred additional planned, you can hopefully understand why I wouldn't want to generate this manually).

So how do you call out to the assembly to generate the help file? Pretty easily! In the Lapointe.PowerShell.MamlGenerator Assembly there is a static class called CmdletHelpGenerator. This class has three static methods that you can call based on how you want to pass in the cmdlet's Assembly information:

public static void GenerateHelp(string outputPath, bool oneFile);
public static void GenerateHelp(string inputFile, string outputPath, bool oneFile);
public static void GenerateHelp(Assembly asm, string outputPath, bool oneFile);

If you want to run this code from PowerShell you can do so using the following syntax:

$asm = "W:\Lapointe.SharePoint2010.Automation.dll"
$cmdletAsm = "W:\Lapointe.PowerShell.MamlGenerator.dll"
$targetPath = "W:\MyProject\POWERSHELL\Help"
[System.Reflection.Assembly]::LoadFrom($asm) | Out-Null
[Lapointe.PowerShell.MamlGenerator.CmdletHelpGenerator]::GenerateHelp($asm, $targetPath, $true)

And again, this works with any cmdlet development, not just SharePoint cmdlet development. Hopefully all you ambitious PowerShell developers out there who appreciate the need to provide your IT Administrators with proper help documentation will find this code as useful as I have.

Good luck and happy PowerShelling!

Tagged as: , 2 Comments