Wednesday, 20 April 2011

Automated SharePoint builds/UI testing–slide deck

It took a few days as I wanted to add screenshots from the demos, but I’ve now published my deck from my talk at the SharePoint Best Practices Conference. I had a great time presenting, and although the prep for this talk was harder than others I’ve done I was happy with how it turned out. I think this subject is starting to get a lot more attention, and you know there’s interest in the topic when you end up giving a repeat demo in the speaker room to fellow speakers who couldn’t attend the talk. The talk is oriented around Team Foundation Server 2010 for the build platform. Here’s a summary of one of the demos:

  • Developer checks in (a breaking change to the data access layer in my demo) and an automated build is triggered
  • Assemblies and WSPs are built (in release mode) and deployed to remote SharePoint server
  • Once deployment is complete, the app pool is warmed up and we start hitting the site with automated UI tests (a feature of Visual Studio 2010 Premium and above)
  • If a test fails, the build is a failure and all developers are notified (via the TFS build notification tool)
  • Build manager/developer checks build report and sees:
    • Screenshot of failing UI test
    • Critical entries from the event log at the time
  • >> Build manager/developer now understands why latest code change broke the build
  • “Added value” bonus demo
    • Same process but with the following data also captured:
      • Code profiling (see which bits of code should be optimized)
      • IntelliTrace (start a debugging session at the exception hit during the test)
      • Video recording of UI tests (not just screenshot)

The idea of course, is that if this information is being surfaced every 24 hours (or perhaps even more frequently from builds throughout the day), then it’s easy to quickly identify problems as code is written. This can lead to a greater chance of success since bugs and other issues are driven out sooner, reducing the cost to fix.

ChrisOBrien_BPC_Small2

Download/view the slide deck

http://slidesha.re/gSQnyD

The future

I got a lot of positive feedback on the talk and it convinced me to do a blog series on this subject. Whilst I know some folks will instantly dismiss this as not being relevant to them, my take is that any SharePoint 2010 project which has some custom code (say above 3 Visual Studio projects) and has Team Foundation Server should at least do the “first level” of automating the build (building assemblies and WSPs) – the build itself takes minutes to configure (though a build server is a prerequisite), and can bring significant benefits.

Also, it looks like I’ll be working with Microsoft (Kirk Evans in particular) on an MSDN whitepaper on the topic. This will be a more formal treatment of many of the principles/techniques I plan to outline in my blog series, and will hopefully become a valuable resource to those looking to implement automated builds.

Wednesday, 23 March 2011

Speaking on nightly SharePoint builds and UI testing - European SharePoint Best Practices Conference 2011

I’m privileged to again be part of this event – it kinda seems strange because it’s on my doorstep here in London, but for SharePoint material this conference is probably only bettered by the official SharePoint Conference held by Microsoft in the U.S (my opinion of course). The speaker list again consists of the biggest names in SharePoint (and me!), and probably the biggest issue an attendee could face is wanting to go to two sessions simultaneously – trust me, there’s worse problems to have with a conference.

After doing 3 sessions last year, I get to focus on just 1 this time so I wanted to make it special. I ended up somewhere in the ALM (Application Lifecycle Management) space again, but I’m hoping it’s something different from some of the other dev sessions (and other talks I’ve given recently). Here’s the abstract:

From good development to great – nightly builds and UI testing with SharePoint 2010 and Team Foundation Server 2010

So you (or your dev team) know your way around the SharePoint API, but deployments are still painful and there are quality issues. Maybe you looked at unit testing SharePoint, but didn't yet manage to fully adopt it. This session looks at how Visual Studio Team Foundation Server can help SharePoint projects, specifically with automated WSP builds and VS2010 UI testing (which can have a much lower barrier to entry than unit testing). When a few of these capabilities are strung together, the results are incredible for dev teams. Over several demos, we'll cover how to get started with automating the build, deploying the resulting WSPs to a remote SharePoint machine, then automatically running UI tests against the site. Part case study, the session will use an innovative SP2010 social/collab implementation (in production at Tesco) as the test bed – with ribbon customizations, a custom service application, and activity feed extensions thrown into the mix.

Hopefully the session will be interesting to anyone who at some point has said “we should get into automated builds”, or indeed, anyone interested in building the kind of social SP2010 intranet we’re building. Note my colleague Wes Hackett also has a community session at the conference on this project.

If you haven’t yet signed up for the Best Practices Conference but are considering it, I highly encourage you to go ahead. Read more on the conference site - European SharePoint Best Practices Conference 2011

Tuesday, 15 March 2011

Optimizing SharePoint 2010 internet sites – slide deck

Last week I gave a talk at Microsoft on this topic, and I thought the slide deck was worth posting. You may remember I recently wrote about Eliminating large JS files to optimize SharePoint 2010 internet sites – my talk mentioned this approach but also had much wider coverage on other techniques to use. Some of the material came from recent experiences of optimizing my current employer’s site (www.contentandcode.com) and also a fairly large client’s social/collab platform – more and more I find that if code and infrastructure aren’t in a really bad place it’s “page-level” optimization which yields the most benefit, and this was the focus of the talk.

I also talked about Aptimize – you might know this product from it’s use on http://sharepoint.microsoft.com. Aptimize automates many of the optimizations I discussed, and I find it pretty interesting as a product. I’ve done some initial testing with it on a site I’ve been involved with and got good results – I’m hoping to look into it further on behalf of a client, and will most likely post further info here.

My slide deck also has some information on:

  • How infrastructure bottlenecks may change in the internet scenario (vs. intranet/collab)
  • How to determine infrastructure bottlenecks
  • Measuring page load times
  • Load testing

You can see the deck on SlideShare at http://slidesha.re/gpAaRc

UPDATE – if you’re interested in performance, I should mention I recorded a SharePoint Podshow episode on the subject with Rob Foster whilst in Redmond recently. It’s not yet published (I’ll update this post when it is) – I talked in some detail about the kind of attention we paid on a client project recently, including around completely “non-code” aspects like disk performance, optimization for SQL’s Temp DB and content database distribution. I’ve no idea how it worked out as an interview, but Rob certainly knows his stuff on performance too so it was a great chat! Look out for it soon.

Thursday, 17 February 2011

CAML.Net Intellisense – now with added Feature upgrade schema

Although many SharePoint developers working with SP2010 are now aware of how useful vital CKS:Dev is, I fear that far fewer are also using John Holliday’s excellent CAML.Net Intellisense. I love this project so much I decided to contribute. If you’re not aware of the tool, the idea is to make it easier to work with the many XML files involved with SharePoint development. It does this by providing detailed documentation “as you type” on the many XML attributes and elements, which so often you’d have open in a separate browser window onto the MSDN docs. There are two huge benefits here:

  • The documentation is generally much more detailed than MSDN. It’s clear that John has spent hours on this.
  • The documentation is right there in Visual Studio, right where you’re working.
    • Additionally, many nodes have direct links into the corresponding detail page on MSDN – so if you do want more detail, you don’t even have to GoogleBing it.

CAML.Net Intellisense existed for SharePoint 2007 development, but John has done a great job rolling things forward for 2010. The tool is now WPF-based and provides a rich (and striking) presentation of the documentation – here’s me working in a file declaring a list instance:

Caml.Net.Intellisense_ListInstance

The tool works across most aspects of SharePoint XML schema, so you get support for declaring Features, fields, content types, modules and so on. Although I’ve typed the first few characters in the screenshot above, the dialog actually appears as soon as you hit space so it’s great for discovering which attributes/child elements hang off the current node.

I did some beta testing on the tool before it was released, and noticed some areas such as the Feature upgrade schema which didn’t have documentation. John was open to me adding these, so after a couple of nights of XSD editing the Intellisense is now useful here too - Here’s me editing a Feature.xml file:

Caml.Net.Intellisense_ApplyElementManifests

..and showing some other elements:

Caml.Net.Intellisense_AddContentTypeField

Caml.Net.Intellisense_CustomUpgradeAction

Don’t stop me now!

If I was reading this post, I’d be saying “Yeah, you know this type of thing is great and everything, but they always slow Visual Studio down. And that makes me mad!”. John mentioned in an e-mail that a design goal for CAML.Net Intellisense was to have zero performance impact on the developer experience – it’s astonishingly fast, and just doesn’t get in the way at all. On that basis, I see no reason why every SharePoint developer shouldn’t install it as a core tool. John’s done a great job and I’m happy to contribute in some minor way. In the future I might look at the giant undertaking of documenting the ribbon schema, but secretly I’m hoping Wictor (or someone else) gets there before me ;)

CAML.Net Intellisense download

Wednesday, 9 February 2011

Repost - SP2010 AJAX part 8: Migrating existing apps to jQuery/AJAX

Special post – if you read my blog through an RSS reader, part 8 of this series may not show up due to a Feedburner screw-up. This is just to let you know that the article is on my site if you want it.

Thursday, 20 January 2011

Eliminating large JS files to optimize SharePoint 2010 internet sites

Back in the SharePoint 2007 timeframe, I wrote my checklist for optimizing SharePoint sites – this was an aggregation of knowledge from various sources (referenced in the article) and from diagnosing performance issues for my clients, and it’s still one of my more popular posts. Nearly all of the recommendations there are still valid for SP 2010, and the core tips like output caching, BLOB caching, IIS compression etc. can have a huge impact on the speed of your site. Those who developed SharePoint internet sites may remember that suppressing large JavaScript files such as core.js was another key step, since SharePoint 2007 added these to every page, even for anonymous users. This meant that the ‘page weight’ for SharePoint pages was pretty bad, with a lot of data going over the wire for each page load. This made SharePoint internet sites slower than they needed to be, since anonymous users didn’t actually need core.js (since it facilitates editing functionality typically only needed for authenticated users) and indeed Microsoft published a workaround using custom code here.

The SP2010 problem

To alleviate some of this problem, SharePoint 2010 introduces the Script On Demand framework (SOD) – this is designed to only send JavaScript files which are actually needed, and in many cases can load them in the background after the page has finished loading. Additionally, the JavaScript files themselves are minified so they are much smaller. Sounds great. However, in my experience it doesn’t completely solve the issue, and there are many variables such as how the developers reference JavaScript files. I’m guessing this is an area where Your Mileage May Vary, but certainly on my current employer’s site (www.contentandcode.com) we were concerned that SP2010 was still adding some heavy JS files for anonymous users, albeit some apparently after page load thanks to SOD. Some of the bigger files were for ribbon functionality, and this seemed crazy since our site doesn’t even use the ribbon for anonymous users. I’ve been asked about the issue several times now, so clearly other people have the same concern. Waldek also has an awesome solution to this problem involving creation of two sets of master pages/page layouts for authenticated/anonymous users, but  that wasn’t an option in our case.

 N.B. Remember that we are primarily discussing the “first-time” user experience here – on subsequent page loads, files will be cached by the browser. However, on internet sites it’s the first-time experience that we tend to care a lot about!

When I use Firebug, I can see that no less than 480KB of JavaScript is being loaded, with an overall page weight of 888KB (and consider that, although this is an image-heavy site, it is fairly optimized with sprite maps for images etc.):

TimelineWithoutScriptsSuppressed2

If we had a way to suppress some of those bigger files for anonymous users entirely, we’d have 123KB of JavaScript with an overall page weight of 478.5KB (70% of it now being the images):

TimelineWithScriptsSuppressed2

But what about page load times?

Right now, if you’ve been paying attention you should be saying “But Chris, those files should be loading after the UI anyway due to Script On Demand, so who cares? Users won’t notice!”. That’s what I thought too. However, this doesn’t seem to add up when you take measurements. I thought long and hard about which tool to measure this with – I decided to use Hammerhead, a tool developed by highly-regarded web performance specialist Steve Souders of Google. Hammerhead makes it easy to hit a website say 10 times, then average the results. As a sidenote, Hammerhead and Firebug do reasssuringly record the same page load time – if you’ve ever wondered about this in Firebug, it’s the red line in Firebug which which we care about. Mozilla documentation defines the blue and red lines (shown in the screenshots above) as:

  • Blue = DOMContentLoaded. Fired when the page's DOM is ready, but the referenced stylesheets, images, and subframes may not be done loading.
  • Red = load. Use the “load” event to detect a fully-loaded page.

Additionally, Hammerhead conveniently simulates first-time site visitors (“Empty cache”) and returning visitors (“Primed cache”) - I’m focusing primary on the first category. Here are the page load times I recorded:

Without large JS files suppressed:

image

With large JS files suppressed:

PageLoadStats_Suppressed

Reading into the page load times

Brief statistics diversion - I suggest we consider both the median and average (arithmetic mean) when comparing, in case you disagree with my logic on this. Personally I think we can use average, since we might have outliers but that’s fairly representative of any server and it’s workload. Anyway, by my maths the differences (using both measures) for a new visitor are:

  • Median – 16% faster with JS suppressed
  • Average – 24% faster with JS suppressed

Either way, I’ll definitely take that for one optimization. We’ve also shaved something off the subsequent page loads which is nice.

The next thing to consider here is network latency. The tests were performed locally on my dev VM – this means that in terms of geographic distance between user and server, it’s approximately 0.0 metres, or 0.000 if you prefer that to 3 decimal places. Unless your global website audience happens to be camped out in your server room, real-life conditions would clearly be ‘worse’ meaning the benefit could be greater than my stats suggest. This would especially be the case if your site has visitors located in other continents to the servers or if users otherwise have slow connections – in these cases, page weight is accepted to be an even bigger factor in site performance than usual.

How it’s done

The approach I took was to prevent SharePoint from adding the unnecessary JS files to the page in the first place. This is actually tricky because script references can originate from anywhere (user controls, web parts, delegate controls etc.) – however, SharePoint typically adds the large JS files using a ClientScriptManager or ScriptLink control and both work the same way. Controls on the page register which JS files they need during the page init cycle (early), and then the respective links get added to the page during the prerender phase (late). Since I know that some files aren’t actually needed, we can simply remove registrations from the collection (it’s in HttpContext.Current.Items) before the rendering happens – this is done via a control in the master page. The bad news is that some reflection is required in the code (to read, not write), but frankly we’re fine with that if it means a faster website. If you’re interested in the details, it’s because it’s not a collection of strings which are stored in HttpContext.Current.Items, but Microsoft.SharePoint.WebControls.ScriptLinkInfo objects (internal).

Control reference (note that files to suppress is configurable):

<!-- the SuppressScriptsForAnonymous control MUST go before the ScriptLink control in the master page -->
<COB:SuppressScriptsForAnonymous runat="server" FilesToSuppress="cui.js;core.js;SP.Ribbon.js" />
<SharePoint:ScriptLink language="javascript" Defer="true" OnDemand="true" runat="server"/> 

The code:

using System;
usingSystem.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Web;
using System.Web.UI;
 
namespace COB.SharePoint.WebControls
{
    /// <summary>
    /// Ensures anonymous users of a SharePoint 2010 site do not receive unnecessary large JavaScript files (slows down first page load). Files to suppress are specified 
    /// in the FilesToSuppress property (a semi-colon separated list). This control *must* be placed before the main OOTB ScriptLink control (Microsoft.SharePoint.WebControls.ScriptLink) in the 
    /// markup for the master page.
    /// </summary>
    /// <remarks>
    /// This control works by manipulating the HttpContext.Current.Items key which contains the script links added by various server-side registrations. Since SharePoint uses sealed/internal 
    /// code to manage this list, some minor reflection is required to read values. However, this is preferable to end-users downloading huge JS files which they do not need.
    /// </remarks>
    [ToolboxData("<{0}:SuppressScriptsForAnonymous runat=\"server\" />")]
    public class SuppressScriptsForAnonymous : Control
    {
        private const string HTTPCONTEXT_SCRIPTLINKS = "sp-scriptlinks";
        private List<string> files = new List<string>();
        private List<int> indiciesOfFilesToBeRemoved = new List<int>();
 
        public string FilesToSuppress
        {
            get;
            set;
        }
        
        protected override void OnInit(EventArgs e)
        {
            files.AddRange(FilesToSuppress.Split(';'));
 
            base.OnInit(e);
        }
 
        protected override void OnPreRender(EventArgs e)
        {
            // only process if user is anonymous..
            if (!HttpContext.Current.User.Identity.IsAuthenticated)
            {
                // get list of registered script files which will be loaded..
                object oFiles = HttpContext.Current.Items[HTTPCONTEXT_SCRIPTLINKS];
                IList registeredFiles = (IList)oFiles;
                int i = 0;
 
                foreach (var file in registeredFiles)
                {
                    // use reflection to get the ScriptLinkInfo.Filename property, then check if in FilesToSuppress list and remove from collection if so..
                    Type t = file.GetType();
                    PropertyInfo prop = t.GetProperty("Filename");
                    if (prop != null)
                    {
                        string filename = prop.GetValue(file, null).ToString();
 
                        if (!string.IsNullOrEmpty(files.Find(delegate(string sFound)
                        {
                            return filename.ToLower().Contains(sFound.ToLower());
                        })))
                        {
                            indiciesOfFilesToBeRemoved.Add(i);
                        }
                    }
 
                    i++;
                }
 
                int iRemoved = 0;
                foreach (int j in indiciesOfFilesToBeRemoved)
                {
                    registeredFiles.RemoveAt(j - iRemoved);
                    iRemoved++;
                }
 
                // overwrite cached value with amended collection.. 
                HttpContext.Current.Items[HTTPCONTEXT_SCRIPTLINKS] = registeredFiles;
            }
            
            base.OnPreRender(e);
        }
    }
}

Usage considerations

For us, this was an entirely acceptable solution. It’s hard to say whether an approach like this would be officially supported, but it would be simple to add a “disable” switch to potentially assuage those concerns for support calls. Ultimately, it doesn’t feel too different to the approach used in the 2007 timeframe to me, but in any case it would be an implementation decision for each deployment and it may not be suitable for all. Interestingly, I’ve shared this code previously with some folks and last I heard it was probably going to be used on a high-traffic *.microsoft.com site running SP2010, so it was interesting for me to hear those guys were fine with it too.

Additionally, you need to consider if your site uses any of the JavaScript we’re trying to suppress. Examples of this could be SharePoint 2010’s modal dialogs, status/notification bars, or Client OM etc.

Finally, even better results could probably be achieved by tweaking the files to suppress (some sites may not need init.js for example), and extending the control to deal with CSS files also. Even if you weren’t to do this, test, test, test of course.

Summary

Although there are many ways to optimize SharePoint internet sites, dealing with page weight is a key step and in SharePoint much of it is caused by JavaScript files which are usually unnecessary for anonymous users. Compression can certainly help here, but comes with a trade-off of additional server load, and it’s not easy to calculate load/benefit to arrive at the right compression level. It seems to me that it would be better to just not send those unnecessary files down the pipe in the first place if we care about performance, and that’s where I went with my approach. I’d love to hear from you if you think my testing or analysis is flawed in any way, since ultimately a good outcome for me would be to discover it’s a problem which doesn’t really need solving so that the whole issue goes away!

Wednesday, 19 January 2011

SP2010 AJAX part 8: Migrating existing apps to jQuery/AJAX

[Apologies if this article appears in your feed as a duplicate – it didn’t go out to many readers originally due to a Feedburner glitch]

  1. Boiling jQuery down to the essentials (technique)
  2. Using the JavaScript Client OM to work with lists (technique)
  3. Using jQuery AJAX with a HTTP handler (technique)
  4. Returning JSON from a HTTP handler (technique)
  5. Enable Intellisense for Client OM and jQuery (tip)
  6. Debugging jQuery/JavaScript (tip)
  7. Useful tools when building AJAX applications (tip)
  8. Migrating existing applications to jQuery/AJAX – this article 

One of the most interesting things I did when getting my head around AJAX/jQuery with SharePoint, was to actually migrate something I’d written over to AJAX. I wanted to do this for a few reasons:

  • It was a real-life app rather than a Hello World
  • It should be a good way to learn
  • I figured demo’ing the steps live would be an interesting segment in my talk at SharePoint Saturday UK

I decided that my SP2010 Feature Upgrade Kit would be a good candidate for this exercise – it’s more of a “mini-app” than a full blown application, in that it really consists of a couple of SharePoint application pages. These deal with the new ‘upgradable’ Features capabilities (read my Feature Upgrade series for more info), but fundamentally it queries a database, displays the records and then allows some or all of them to be updated – in other words, a fairly typical CRUD app which would be good to AJAX-ify. 

FeatureUpgradeKit_NonAjax

FeatureUpgradeKit_Upgraded_NonAjax

Needless to say, it’s all standard ASP.Net/SharePoint controls, so a postback happens when you do a search/update records etc.

The original code

If you’re in any way interested in the upgrade process (or the mindset for building AJAX apps), it’s probably worth having a quick scroll to get feel for the original code at this point.

The ASPX page:

   1: <%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
   2: <%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
   3: <%@ Import Namespace="Microsoft.SharePoint" %>
   4: <%@ Import Namespace="Microsoft.SharePoint.WebControls" %>
   5: <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
   6: <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
   7: <%@ Register Tagprefix="asp" Namespace="System.Web" Assembly="System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" %>
   8: <%@ Register Tagprefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
   9: <%@ Assembly Name="Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
  10: <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="CentralAdminFeatureUpgrade.aspx.cs" Inherits="COB.SharePoint.Utilities.FeatureUpgradeKit.CentralAdminFeatureUpgrade" DynamicMasterPageFile="~masterurl/default.master" %>
  11: <%@ Register TagPrefix="wssuc" TagName="InputFormSection" src="/_controltemplates/InputFormSection.ascx" %> 
  12: <%@ Register TagPrefix="wssuc" TagName="InputFormControl" src="/_controltemplates/InputFormControl.ascx" %> 
  13: <%@ Register TagPrefix="wssuc" TagName="LinkSection" src="/_controltemplates/LinkSection.ascx" %> 
  14: <%@ Register TagPrefix="wssuc" TagName="ButtonSection" src="/_controltemplates/ButtonSection.ascx" %> 
  15: <%@ Register TagPrefix="wssuc" TagName="ActionBar" src="/_controltemplates/ActionBar.ascx" %> 
  16: <%@ Register TagPrefix="wssuc" TagName="ToolBar" src="/_controltemplates/ToolBar.ascx" %> 
  17: <%@ Register TagPrefix="wssuc" TagName="ToolBarButton" src="/_controltemplates/ToolBarButton.ascx" %> 
  18: <%@ Register TagPrefix="wssuc" TagName="Welcome" src="/_controltemplates/Welcome.ascx" %>
  19:  
  20: <asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
  21:   <link rel="stylesheet" type="text/css" href="/_layouts/COB/Styles/COB.SharePoint.FeatureUpgradeKit.css"/>
  22:   <script type="text/javascript" src="/_layouts/jquery-1.4.1.min.js"></script>
   1:  
   2:   <script type="text/javascript" src="/_layouts/COB/js/FeatureUpgradeSliding.js">
   1: </script>
   2:   <script type="text/javascript" src="/_layouts/COB/js/FeatureUpgrade.js">
   1: </script>
   2:   <% if (false) { %>
   3:     <script type="text/javascript" src="../../../layouts/jquery-1.4.1-vsdoc.js">
</script>
  23:   <%
   1:  } 
%>
  24: <table class="propertysheet" border="0" width="100%" cellspacing="0" cellpadding="0">
  25:     <div id="introText">
  26:         By Chris O'Brien - <a target="_blank" href="http://www.sharepointnutsandbolts.com">www.sharepointnutsandbolts.com</a>
  27:         <br />
  28:     </div>
  29:     <wssuc:InputFormSection ID="scopeSection" Title="Select Feature scope to query for"
  30:                      runat="server">
  31:                      <Template_Description>
  32:                                   <SharePoint:EncodedLiteral ID="EncodedLiteral1" runat="server" text="Select the scope of the Features are being upgraded" EncodeMethod='HtmlEncode'/>
  33:                      </Template_Description>
  34:                      <Template_InputFormControls>
  35:                            <wssuc:InputFormControl SmallIndent="true" runat="server">
  36:   <Template_Control>
  37:                         <asp:dropdownlist ID="ddlScopes" runat="server">
  38:                             <asp:ListItem ID="liFarm" runat="server" Value="Farm" />
  39:                             <asp:ListItem ID="liWebApp" runat="server" Value="WebApplication" />
  40:                             <asp:ListItem ID="liSite" runat="server" Value="Site" />
  41:                             <asp:ListItem ID="liWeb" runat="server" Value="Web" />
  42:                         </asp:dropdownlist>
  43:                                   </Template_Control>
  44:                            </wssuc:InputFormControl>
  45:                  </Template_InputFormControls>
  46:               </wssuc:InputFormSection>
  47:          <wssuc:InputFormSection ID="webAppSection" Title="Web application"
  48:                      runat="server" style="display:none;">
  49:                      <Template_Description>
  50:                                   <SharePoint:EncodedLiteral ID="EncodedLiteral2" runat="server" text="Select the web application where you wish to upgrade site-scoped Features" EncodeMethod='HtmlEncode'/>
  51:                      </Template_Description>
  52:                      <Template_InputFormControls>
  53:                            <wssuc:InputFormControl SmallIndent="true" runat="server">
  54:                                   <Template_Control>
  55:                         <SharePoint:WebApplicationSelector runat=server ID="webAppSelector" UseDefaultSelection="true" />
  56:                                   </Template_Control>
  57:                            </wssuc:InputFormControl>
  58:                   </Template_InputFormControls>
  59:               </wssuc:InputFormSection>
  60:         <wssuc:InputFormSection ID="siteSection" Title="Site Collection"
  61:                      runat="server" style="display:none;">
  62:                      <Template_Description>
  63:                                   <SharePoint:EncodedLiteral ID="EncodedLiteral3" runat="server" text="Select the web application and site collection where you wish to upgrade web-scoped Features" EncodeMethod='HtmlEncode'/>
  64:                      </Template_Description>
  65:                      <Template_InputFormControls>
  66:                            <wssuc:InputFormControl SmallIndent="true" runat="server">
  67:                                   <Template_Control>
  68:                         <SharePoint:SiteAdministrationSelector runat=server ID="siteCollectionSelector" />
  69:                                   </Template_Control>
  70:                            </wssuc:InputFormControl>
  71:                   </Template_InputFormControls>
  72:               </wssuc:InputFormSection>
  73:         <wssuc:InputFormSection ID="featureFilterSection" Title="Feature status"
  74:                      runat="server">
  75:                      <Template_Description>
  76:                                   <SharePoint:EncodedLiteral ID="EncodedLiteral4" runat="server" text="Display only Features which need upgrade?" EncodeMethod='HtmlEncode'/>
  77:                      </Template_Description>
  78:                      <Template_InputFormControls>
  79:                            <wssuc:InputFormControl SmallIndent="true" runat="server">
  80:                                   <Template_Control>
  81:                         <asp:RadioButtonList ID="rblUpgradeOnly" runat="server">
  82:                             <asp:ListItem
  83:                                 Selected="True"
  84:                                 Text="Only Features requiring upgrade"
  85:                                 Value="NeedUpgrade" />
  86:                             <asp:ListItem
  87:                                 Selected="False"
  88:                                 Text="All Features"
  89:                                 Value="NoUpgrade" />
  90:                         </asp:RadioButtonList>
  91:                                   </Template_Control>
  92:                            </wssuc:InputFormControl>
  93:                  </Template_InputFormControls>
  94:               </wssuc:InputFormSection>
  95:         <wssuc:InputFormSection ID="searchSection" Title="Click 'Search' to run the query"
  96:                      runat="server">
  97:                      <Template_InputFormControls>
  98:                            <wssuc:InputFormControl SmallIndent="true" runat="server">
  99:                                   <Template_Control>
 100:                         <asp:Button runat="server" class="ms-ButtonHeightWidth" Text="Search" id="searchButton" OnClick="searchButton_Click" />
 101:                                   </Template_Control>
 102:                            </wssuc:InputFormControl>
 103:                  </Template_InputFormControls>
 104:               </wssuc:InputFormSection>
 105:      </table>
 106:     
 107:     <div id="resultsContainer">
 108:         <asp:Panel ID="pnlNoEntries" CssClass="noResults" runat="server" Visible="false">
 109:             <asp:Label runat="server" ID="lblMessage" Text="There are no Feature instances requiring upgrade at this scope." />    
 110:         </asp:Panel>
 111:         <asp:Panel ID="pnlUpgradeSuccess" CssClass="upgradeSuccess" runat="server" Visible="false">
 112:             <strong>The following Feature instances were upgraded successfully:</strong> <br /><br />
 113:             <asp:Label ID="lblSuccesses" runat="server" />
 114:         </asp:Panel>
 115:         <asp:Panel ID="pnlUpgradeFailure" CssClass="upgradeFailure" runat="server" Visible="false">
 116:             <strong>The following Feature instances failed to upgrade:</strong> <br /><br />
 117:             <asp:Label ID="lblErrors" runat="server" />
 118:         </asp:Panel>
 119:         <asp:Panel ID="pnlGrid" runat="server">
 120:             <br />
 121:             <SharePoint:SPGridView 
 122:               runat="server" 
 123:               ID="grdFeatures" 
 124:               AutoGenerateColumns="false"
 125:               RowStyle-BackColor="#DDDDDD"
 126:               AlternatingRowStyle-BackColor="#EEEEEE" ShowHeaderWhenEmpty="true">
 127:                 <EmptyDataTemplate>
 128:                 </EmptyDataTemplate>
 129:               </SharePoint:SPGridView>
 130:  
 131:                <div id="upgradeControlsNonAjax">
 132:                     <span id="upgradeControlsLeft">
 133:                         &nbsp;
 134:                     </span>
 135:                     <span id="upgradeControlsRight">
 136:                                <asp:Button runat="server" class="ms-ButtonHeightWidth upgradeButton" Text="Upgrade Features" id="topOKButton" OnClick="okButton_Click" accesskey="<%$Resources:wss,okbutton_accesskey%>"/>
 137:                     </span>
 138:                </div>
 139:           </asp:Panel>
 140:           <br />
 141:           <asp:Label ID="lblResults" Visible="false" runat="server" />
 142:       </div>
 143: </asp:Content>
 144:  
 145: <asp:Content ID="PageTitle" ContentPlaceHolderID="PlaceHolderPageTitle" runat="server">
 146: Feature upgrade
 147: </asp:Content>
 148:  
 149: <asp:Content ID="PageTitleInTitleArea" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server" >
 150: Feature upgrade
 151: </asp:Content>

Code-behind:

   1: using System;
   2: using Microsoft.SharePoint;
   3: using Microsoft.SharePoint.WebControls;
   4: using System.Web.UI.WebControls;
   5: using System.Collections;
   6: using System.Web.UI;
   7: using System.Data;
   8: using Microsoft.SharePoint.Administration;
   9: using System.Collections.Generic;
  10: using System.Linq;
  11: using COB.SharePoint.Utilities.FeatureUpgradeKit.Entities;
  12: using COB.SharePoint.Utilities.FeatureUpgradeKit.Core;
  13:  
  14: namespace COB.SharePoint.Utilities.FeatureUpgradeKit
  15: {
  16:     /// <summary>
  17:     /// Provides an administration page for upgrading Features in SharePoint.
  18:     /// </summary>
  19:     /// <created by="Chris O'Brien" date="09 July 2010" />
  20:     public partial class CentralAdminFeatureUpgrade : LayoutsPageBase
  21:     {
  22:         #region -- Fields and child classes --
  23:  
  24:         private Hashtable fields = new Hashtable();
  25:         private DataTable dtFeatures = null;
  26:         private const string NeedUpgradeValue = "NeedUpgrade";
  27:         public const string UpgradeCheckBoxId = "chkUpgrade";
  28:  
  29:         private class FeatureFields
  30:         {
  31:             public const string DisplayName = "DisplayName";
  32:             public const string Version = "Version";
  33:             public const string Parent = "Parent";
  34:             public const string DoUpgrade = "DoUpgrade";
  35:             public const string FeatureID = "FeatureID";
  36:             public const string FeatureScope = "Scope";
  37:             public const string Identifiers = "Identifiers";
  38:         }
  39:  
  40:         private class GridHeaders
  41:         {
  42:             public const string DisplayName = "Feature name";
  43:             public const string Version = "Current version";
  44:             public const string Parent = "Parent";
  45:             public const string DoUpgrade = "Upgrade?";
  46:             public const string FeatureID = "Feature ID";
  47:             public const string FeatureScope = "Scope";
  48:             public const string Identifiers = "Identifiers";
  49:         }
  50:  
  51:         #endregion
  52:  
  53:         #region -- Page event handlers --
  54:  
  55:         protected override void OnInit(EventArgs e)
  56:         {
  57:             // initialise SPGridView etc..
  58:             addColumns();
  59:             grdFeatures.AllowSorting = false;
  60:             
  61:             topOKButton.Visible = false;
  62:             base.OnInit(e);
  63:         }
  64:  
  65:         protected override void OnLoad(EventArgs e)
  66:         {
  67:             // any postback on this page is caused by controls related to querying Features/working with grid, so we'll rebind data on any postback..
  68:             if (Page.IsPostBack)
  69:             {
  70:                 populateFeaturesTable();
  71:                 bindData(dtFeatures);
  72:             }
  73:  
  74:             base.OnLoad(e);
  75:         }
  76:  
  77:         protected void searchButton_Click(object sender, EventArgs e)
  78:         {
  79:             if (dtFeatures != null)
  80:             {
  81:                 topOKButton.Visible = (dtFeatures.Rows.Count > 0 && rblUpgradeOnly.SelectedValue == NeedUpgradeValue);
  82:                 pnlNoEntries.Visible = (dtFeatures.Rows.Count == 0);
  83:                 pnlUpgradeFailure.Visible = false;
  84:                 pnlUpgradeSuccess.Visible = false;
  85:             }
  86:         }
  87:  
  88:         protected void okButton_Click(object sender, EventArgs e)
  89:         {
  90:             clearMessages();
  91:  
  92:             List<FeatureDetails> upgradeIdentifiers = new List<FeatureDetails>();
  93:             foreach (SPGridViewRow row in grdFeatures.Rows)
  94:             {
  95:                 Control c = row.FindControl(UpgradeCheckBoxId);
  96:                 CheckBox chk = c as CheckBox;
  97:                 if ((chk != null) && (chk.Checked))
  98:                 {
  99:                     upgradeIdentifiers.Add(FeatureDetails.CreateFromString(grdFeatures.DataKeys[row.RowIndex].Value.ToString()));
 100:                 }
 101:             }
 102:  
 103:             // upgrade selected Features and display results..
 104:             SPSecurity.RunWithElevatedPrivileges(delegate() { 
 105:                 Dictionary<FeatureDetails, IEnumerable<Exception>> upgradeResults = FeatureManager.UpgradeFeatures(upgradeIdentifiers);
 106:                 presentResultMessages(upgradeResults);
 107:             });
 108:  
 109:             // now re-run query to update grid..
 110:             populateFeaturesTable();
 111:             bindData(dtFeatures);
 112:  
 113:             if (dtFeatures.Rows.Count == 0)
 114:             {
 115:                 pnlNoEntries.Visible = false;
 116:                 topOKButton.Visible = false;
 117:             }
 118:         }
 119:  
 120:         #endregion
 121:  
 122:         #region -- UI handling --
 123:  
 124:         private void presentResultMessages(Dictionary<FeatureDetails, IEnumerable<Exception>> upgradeResults)
 125:         {
 126:             int errorCount = 0;
 127:             foreach (KeyValuePair<FeatureDetails, IEnumerable<Exception>> kvp in upgradeResults)
 128:             {
 129:                 List<Exception> exceptions = null;
 130:                 if (kvp.Value != null)
 131:                 {
 132:                     exceptions = kvp.Value.ToList();
 133:                 }
 134:  
 135:                 if (exceptions != null && exceptions.Count > 0)
 136:                 {
 137:                     pnlUpgradeFailure.Visible = true;
 138:                     if (errorCount > 0)
 139:                     {
 140:                         lblErrors.Text += "<br />";
 141:                     }
 142:                     lblErrors.Text += string.Format("Feature '{0}' with parent '{1}' failed with the following errors:<br /><br />",
 143:                         kvp.Key.FeatureName, kvp.Key.ParentString);
 144:                     foreach (Exception exception in exceptions)
 145:                     {
 146:                         lblErrors.Text += string.Format("  - {0}<br />", exception.ToString());
 147:                         errorCount += exceptions.Count;
 148:                     }
 149:                 }
 150:                 else
 151:                 {
 152:                     pnlUpgradeSuccess.Visible = true;
 153:                     lblSuccesses.Text += string.Format("Feature '{0}' with parent '{1}' upgraded successfully.<br />",
 154:                         kvp.Key.FeatureName, kvp.Key.ParentString);
 155:                 }
 156:             }
 157:         }
 158:  
 159:         private void clearMessages()
 160:         {
 161:             lblErrors.Text = string.Empty;
 162:             lblSuccesses.Text = string.Empty;
 163:             lblResults.Text = string.Empty;
 164:         }
 165:  
 166:         private void bindData(DataTable dtFeatures)
 167:         {
 168:             // assign default sort expression..
 169:             DataView sortedView = new DataView(dtFeatures);
 170:             sortedView.Sort = FeatureFields.DisplayName + " ASC";
 171:  
 172:             // add columns to grid..
 173:             grdFeatures.Columns.Clear();
 174:  
 175:             foreach (DictionaryEntry de in fields)
 176:             {
 177:                 bool visibleField = (de.Key.ToString() != GridHeaders.Identifiers);
 178:                 addBoundField(de.Key.ToString(), de.Value.ToString(), visibleField);
 179:             }
 180:  
 181:             if (rblUpgradeOnly.SelectedValue == NeedUpgradeValue)
 182:             {
 183:                 // add checkbox column..
 184:                 addUpgradeCheckBox(grdFeatures);
 185:             }
 186:  
 187:             grdFeatures.DataKeyNames = new string[] { FeatureFields.Identifiers };
 188:             grdFeatures.DataSource = sortedView;
 189:             grdFeatures.DataBind();
 190:         }
 191:  
 192:         private static void addUpgradeCheckBox(SPGridView gridView)
 193:         {
 194:             TemplateField upgradeField = new TemplateField
 195:             {
 196:                 HeaderText = GridHeaders.DoUpgrade,
 197:                 ItemTemplate = new UpgradeCheckBoxGridviewTemplate()
 198:             };
 199:  
 200:             gridView.Columns.Add(upgradeField);
 201:         }
 202:  
 203:         private void addBoundField(string headerText, string dataField, bool visibleField)
 204:         {
 205:             SPBoundField userField = new SPBoundField();
 206:             userField.HeaderText = headerText;
 207:             userField.DataField = dataField;
 208:             userField.SortExpression = headerText;
 209:             userField.Visible = visibleField;











 210:             grdFeatures.Columns.Add(userField);
 211:         }
 212:  
 213:         private static Control findControlRecursive(Control Root, string Id)
 214:         {
 215:             if (Root.ID == Id)
 216:                 return Root;
 217:  
 218:             foreach (Control Ctl in Root.Controls)
 219:             {
 220:                 Control FoundCtl = findControlRecursive(Ctl, Id);
 221:                 if (FoundCtl != null)
 222:                     return FoundCtl;
 223:             }
 224:  
 225:             return null;
 226:         }
 227:  
 228:  
 229:         #endregion
 230:  
 231:         #region -- Feature handling --
 232:  
 233:         private void populateFeaturesTable()
 234:         {
 235:             string siteUrl = SPContext.Current.Site.Url;
 236:             DropDownList ddlScopes = (DropDownList)findControlRecursive(scopeSection, "ddlScopes");
 237:             RadioButtonList rblUpgradeOnly = (RadioButtonList)findControlRecursive(featureFilterSection, "rblUpgradeOnly");
 238:             bool upgradeOnly = (rblUpgradeOnly.SelectedValue == NeedUpgradeValue);
 239:             SPFeatureScope scope = (SPFeatureScope)Enum.Parse(typeof(SPFeatureScope), ddlScopes.SelectedValue);
 240:             SPFeatureQueryResultCollection featuresForUpgrade = null;
 241:  
 242:             switch (scope)
 243:             {
 244:                 case SPFeatureScope.Farm:
 245:                     featuresForUpgrade = SPWebService.AdministrationService.QueryFeatures(scope, upgradeOnly);
 246:                     break;
 247:                 case SPFeatureScope.WebApplication:
 248:                     featuresForUpgrade = SPWebService.QueryFeaturesInAllWebServices(scope, upgradeOnly);
 249:                     break;
 250:                 case SPFeatureScope.Site:
 251:                     featuresForUpgrade = webAppSelector.CurrentItem.QueryFeatures(scope, upgradeOnly);
 252:                     break;
 253:                 case SPFeatureScope.Web:
 254:                     using (SPSite selectedSite = new SPSite(siteCollectionSelector.CurrentItem.Url))
 255:                     {
 256:                         featuresForUpgrade = selectedSite.QueryFeatures(scope, upgradeOnly);
 257:                     }
 258:                     break;
 259:                 default:
 260:                     throw new NotImplementedException(string.Format("Cannot use this page to upgrade Features at scope '{0}'!", scope));
 261:             }
 262:             
 263:             dtFeatures = getDataTable(featuresForUpgrade);
 264:         }
 265:  
 266:         private DataTable getDataTable(SPFeatureQueryResultCollection featuresForUpgrade)
 267:         {
 268:             DataTable dtEntries = new DataTable("FeatureQueryResult");
 269:             dtEntries.Columns.Add(new DataColumn(FeatureFields.DisplayName));
 270:             dtEntries.Columns.Add(new DataColumn(FeatureFields.Parent));
 271:             dtEntries.Columns.Add(new DataColumn(FeatureFields.Version));
 272:             dtEntries.Columns.Add(new DataColumn(FeatureFields.FeatureID));
 273:             dtEntries.Columns.Add(new DataColumn(FeatureFields.FeatureScope));
 274:             dtEntries.Columns.Add(new DataColumn(FeatureFields.Identifiers));
 275:  
 276:             foreach (SPFeature feature in featuresForUpgrade)
 277:             {
 278:                 string parent = FeatureManager.GetFeatureParent(feature);
 279:                 DataRow dr = dtEntries.NewRow();
 280:                 dr[FeatureFields.DisplayName] = feature.Definition.DisplayName;
 281:                 dr[FeatureFields.Parent] = parent;
 282:                 dr[FeatureFields.Version] = feature.Version;
 283:                 dr[FeatureFields.FeatureID] = feature.DefinitionId;
 284:                 dr[FeatureFields.FeatureScope] = feature.Definition.Scope;
 285:                 dr[FeatureFields.Identifiers] = new FeatureDetails()
 286:                 {
 287:                     FeatureScope = feature.Definition.Scope,
 288:                     FeatureID = feature.DefinitionId,
 289:                     ParentID = new Guid(FeatureManager.GetFeatureParentId(feature).ToString()),
 290:                     GrandParentID = new Guid(FeatureManager.GetFeatureGrandParentId(feature).ToString()),
 291:                     FeatureName = feature.Definition.DisplayName,
 292:                     ParentString = parent
 293:                 }.ToString();
 294:  
 295:                 dtEntries.Rows.Add(dr);
 296:             }
 297:         
 298:             return dtEntries;
 299:         }
 300:  
 301:         #endregion
 302:  
 303:         #region -- Misc helpers --
 304:  
 305:         private void addColumns()
 306:         {
 307:             fields.Add(GridHeaders.DisplayName, FeatureFields.DisplayName);
 308:             fields.Add(GridHeaders.Version, FeatureFields.Version);
 309:             fields.Add(GridHeaders.Parent, FeatureFields.Parent);
 310:             fields.Add(GridHeaders.FeatureID, FeatureFields.FeatureID);
 311:             fields.Add(GridHeaders.FeatureScope, FeatureFields.FeatureScope);
 312:             fields.Add(GridHeaders.Identifiers, FeatureFields.Identifiers);
 313:         }
 314:         
 315:         
 316:  
 317:         #endregion
 318:     }
 319: }

Changes required for AJAX

Here’s the process I went through to covert the app to AJAX – I’ll list the code lower down (and post the solution file) if you want to look at any specifics. If you’re new to building the AJAX way, I personally think it’s quite eye-opening:

  1. Delete ALL the code in the code-behind. You Ain’t Gonna Need It (in this form), even if it did take you a few late nights to write it :)
  2. In the .aspx, remove any of the ASP.Net controls used in the app and switch them to good ol’ fashioned HTML controls. For example:
    1. The dropdown for the Feature scope is changed from a <asp:dropdownlist> control to a <select> control.
    2. The ‘Feature status’ radio buttons are changed from a <asp:RadioButtonList> to a <input type="radio">.
    3. The ‘Search’ and ‘Upgrade Features’ buttons are changed from <asp:Button> to <button>.
    4. The <asp:Panel> and <asp:Label> controls used to present results messages are removed completely.
    5. The SPGridView control used to display records is also removed – for these last 2 items, you’ll see that I use jQuery to insert HTML into the parent div instead.

      Note that often you’ll want to keep many other ASP.Net controls – in my case I kept the InputFormSection, WebApplicationSelector and SiteCollectionSelector controls, since these are SharePoint controls responsible for application page look and feel, and selecting the web app/site collection respectively. In terms of the selector controls, of course I’ll still need to ‘get’ the selected value but it turns out it’s entirely possible to do this with jQuery instead of server side code.
  3. Introduce some jQuery code:
    1. Event binding code on page load – here we bind event handlers to HTML the button clicks.
    2. The following jQuery methods:
      1. getWebAppSelectorValue() – since a Microsoft.SharePoint.WebControls.ApplicationSelector control actually renders a div containing a link and span, the following jQuery will fetch the value (assuming the HTML ID is correct) - $('a#zz1_webAppSelector span').text().
      2. getWebAppSelectorValue() – similar to above.
      3. displayFeaturesTable() – does the work of fetching the records (list of Features to be upgraded) and displaying them in a HTML table similar to the output of a SPGridView. Key method – this calls into a HTTP handler using jQuery AJAX (as shown in article 3, using jQuery AJAX with a HTTP handler) which queries SharePoint for Features to upgrade, and ultimately injects the HTML for the results table into the page.
      4. upgradeFeatures() – does the work of updating the selected records (upgrading the Features) and displaying the results. Key method – this calls into a different method on the HTTP handler using jQuery AJAX, passing the IDs of Features to upgrade, collecting the results from the server and updating the page accordingly.
  4. Introduce the HTTP handler – generally this is written simultaneously with the calling jQuery. I detect parameters in the URL to know which action is being processed (i.e. fetching the records or updating the records). Add code for each:
    1. In the ‘QueryFeatures’ branch of the code, fetch a list of Features requiring upgrade, then JSON-serialize the data and return (as shown in article 4, Returning JSON from a HTTP handler)
    2. In the ‘UpgradeFeatures’ branch, write code to resolve Features from the passed IDs, then call SPFeature.Upgrade() on each. Finally, return a JSON-serialized object containing the results.

The result

Once the changes were made, the app definitely was a lot slicker without postbacks. Although screenshots can’t show it, the grid fades in smoothly once the results are returned (courtesy of a jQuery fadeIn()) and upgrading the Features has a similar experience. If anything, there was a (somewhat ironic) new problem that everything happened so fast that it didn’t seem like the processing work had been done properly.

If you want to download the code to take a close look, there’s a link at the end of the article. Otherwise here’s what the jQuery looked like:

   1: $(function () {
   2:     $.ajaxSetup({ cache: false });
   3:     $('#upgradeControls').hide();
   4:  
   5:     // bind event handlers..
   6:     $('#btnSearch').click(displayFeaturesTable);
   7:     $('#OKButton').click(upgradeFeatures);
   8: });
   9:  
  10:  
  11: function getWebAppSelectorValue() {
  12:     return $('a#zz1_webAppSelector span').text();
  13: }
  14:  
  15: function getSiteColSelectorValue() {
  16:     return $('a#zz2_siteCollectionSelector span').text();
  17: }
  18:  
  19: function displayFeaturesTable() {
  20:     $('#resultsGrid').hide();
  21:     var requiringUpgradeOnly = $('.rblUpgradeOnly:checked').val();
  22:     var scopeValue = $('#ddlScopes').val();
  23:     var parentString = null;
  24:     if (scopeValue == 'Site') {
  25:         parentString = getWebAppSelectorValue();
  26:     }
  27:     if (scopeValue == 'Web') {
  28:         parentString = getSiteColSelectorValue();
  29:     }
  30:     $.get('/_admin/COB/FeatureUpgradeCompleted.cob',
  31:                           { op: 'QueryFeatures', scope: scopeValue, upgradeOnly: requiringUpgradeOnly, parentUrl: parentString }, function (data) {
  32:                               if (data.ResultCount > 0) {
  33:                                   $('#upgradeControls').fadeIn();
  34:                                   $('#resultsGrid').html(data.Html).fadeIn();
  35:                               }
  36:                               else {
  37:                                   $('#resultsGrid').html("<div class=\"noResults\">There are no Feature instances requiring upgrade at this scope.</div>").fadeIn();
  38:                                   $('#upgradeControls').hide();
  39:                               }
  40:                           }
  41:                       );
  42:                 };
  43:  
  44:  
  45: function upgradeFeatures() {
  46:     var featureDetails = $('.chkUpgrade:checked').map(function () {
  47:         return this.id;
  48:     }).get().join(',');
  49:  
  50:     var notificationId = SP.UI.Notify.addNotification('Feature upgrade started..', true);
  51:     SP.UI.Status.removeAllStatus(true);
  52:     $.ajax({
  53:         cache: false,
  54:         type: "GET",
  55:         dataType: "json",
  56:         url: '/_admin/COB/FeatureUpgradeCompleted.cob',
  57:         data: { "op": "UpgradeFeatures", "Features": featureDetails },
  58:         success: function (data) {
  59:             SP.UI.Notify.removeNotification(notificationId);
  60:             var successUpgrades = new Array();
  61:             var failedUpgrades = new Array();
  62:             var statusId;
  63:             var statusColour;
  64:  
  65:             if (data.HasErrors) {
  66:                 statusColour = 'red';
  67:             }
  68:             else {
  69:                 statusColour = 'green';
  70:             }
  71:  
  72:             $.each(data.Results, function (key, value) {
  73:                 if (value.Exceptions != null) {
  74:                     var failedMsg = "Feature '" + value.FeatureDetails.FeatureName + "' with parent <a target='_blank' href='" + value.FeatureDetails.ParentString + "'>" + value.FeatureDetails.ParentString + "</a> failed to upgrade. Error message: " + value.Exceptions[0].Message;
  75:                     failedUpgrades.push(failedMsg);
  76:                 }
  77:                 else {
  78:                     var successMsg = "Feature '" + value.FeatureDetails.FeatureName + "' with parent <a target='_blank' href='" + value.FeatureDetails.ParentString + "'>" + value.FeatureDetails.ParentString + "</a> upgraded sucessfully.";
  79:                     successUpgrades.push(successMsg);
  80:                 }
  81:             })
  82:  
  83:             $.each(failedUpgrades, function (index, value) {
  84:                 statusId = SP.UI.Status.addStatus("<u>Failed:</u> ", "<strong>" + value + "</strong>", false);
  85:                 SP.UI.Status.setStatusPriColor(statusId, statusColour);
  86:             });
  87:  
  88:             $.each(successUpgrades, function (index, value) {
  89:                 statusId = SP.UI.Status.addStatus("<u>Success:</u> ", "<strong>" + value + "</strong>", false);
  90:                 SP.UI.Status.setStatusPriColor(statusId, statusColour);
  91:             });
  92:  
  93:             displayFeaturesTable();
  94:         },
  95:         failure: function (data) {
  96:             SP.UI.Notify.removeNotification(notificationId);
  97:             alert("An error occurred whilst upgrading Features - " + data);
  98:         }
  99:     });
 100: }
 101:     
 102:     
 103:     

And the HTTP handler:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Web;
   6: using System.Web.Script.Serialization;
   7: using COB.SharePoint.Utilities.FeatureUpgradeKit.Core;
   8: using COB.SharePoint.Utilities.FeatureUpgradeKit.Core.Entities;
   9: using COB.SharePoint.Utilities.FeatureUpgradeKit.Entities;
  10: using Microsoft.SharePoint;
  11: using Microsoft.SharePoint.Administration;
  12:  
  13: namespace COB.SharePoint.Utilities.FeatureUpgradeKit.Handlers
  14: {
  15:     public class CentralAdminUpgradeHandlerCompleted : IHttpHandler
  16:     {
  17:         private const string qsOperation = "op";
  18:         private const string qsScope = "scope";
  19:         private const string qsUpgradeOnly = "upgradeOnly";
  20:         private const string qsParentUrl = "parentUrl";
  21:         private const string qsFeaturesForUpgrade = "Features";
  22:  
  23:         #region -- Helper methods --
  24:  
  25:         private List<SPFeature> getFeaturesForUpgrade(SPFeatureScope scope, bool upgradeOnly, string parentUrl)
  26:         {
  27:             SPFeatureQueryResultCollection featuresForUpgrade = null;
  28:  
  29:             switch (scope)
  30:             {
  31:                 case SPFeatureScope.Farm:
  32:                     featuresForUpgrade = SPWebService.AdministrationService.QueryFeatures(scope, upgradeOnly);
  33:                     break;
  34:                 case SPFeatureScope.WebApplication:
  35:                     featuresForUpgrade = SPWebService.QueryFeaturesInAllWebServices(scope, upgradeOnly);
  36:                     break;
  37:                 case SPFeatureScope.Site:
  38:                     SPWebApplication webApp = SPWebApplication.Lookup(new Uri(parentUrl));
  39:                     featuresForUpgrade = webApp.QueryFeatures(scope, upgradeOnly);
  40:                     break;
  41:                 case SPFeatureScope.Web:
  42:                     using (SPSite selectedSite = new SPSite(parentUrl))
  43:                     {
  44:                         featuresForUpgrade = selectedSite.QueryFeatures(scope, upgradeOnly);
  45:                     }
  46:                     break;
  47:                 default:
  48:                     throw new NotImplementedException(string.Format("Cannot use this page to upgrade Features at scope '{0}'!", scope));
  49:             }
  50:  
  51:             List<SPFeature> featuresForUpgradeList = null;
  52:             featuresForUpgradeList = featuresForUpgrade.ToList<SPFeature>();
  53:  
  54:             return featuresForUpgradeList;
  55:         }
  56:  
  57:         private Dictionary<string, FeatureUpgradeResultDetail> getSimplifiedResults(Dictionary<FeatureDetails, IEnumerable<Exception>> upgradeResults)
  58:         {
  59:             Dictionary<string, FeatureUpgradeResultDetail> simplifiedResults = new Dictionary<string, FeatureUpgradeResultDetail>();
  60:  
  61:             foreach (KeyValuePair<FeatureDetails, IEnumerable<Exception>> kvp in upgradeResults)
  62:             {
  63:                 simplifiedResults.Add(kvp.Key.ToString(), new FeatureUpgradeResultDetail { FeatureDetails = kvp.Key, Exceptions = kvp.Value });
  64:             }
  65:  
  66:             return simplifiedResults;
  67:         }
  68:  
  69:         #endregion
  70:  
  71:         #region -- Fetch data/grid methods --
  72:  
  73:         private string buildQueryFeaturesResponse(HttpContext context, List<SPFeature> featuresToProcess)
  74:         {
  75:             StringBuilder sbFeaturesTable = new StringBuilder();
  76:  
  77:             if (featuresToProcess.Count > 0)
  78:             {
  79:                 writeTableHeader(sbFeaturesTable);
  80:                 writeTableRows(sbFeaturesTable, featuresToProcess);
  81:                 writeTableFooter(sbFeaturesTable);
  82:             }
  83:          
  84:             return sbFeaturesTable.ToString();
  85:         }
  86:  
  87:         private void writeTableHeader(StringBuilder builder)
  88:         {
  89:             builder.AppendFormat("<TABLE style=\"BORDER-BOTTOM-STYLE: none; BORDER-RIGHT-STYLE: none; WIDTH: 100%; BORDER-COLLAPSE: collapse; BORDER-TOP-STYLE: none; BORDER-LEFT-STYLE: none\" " +
  90:                 "id=tblFeatures class=ms-listviewtable border=0 cellSpacing=0 ShowHeaderWhenEmpty=true><TBODY>" +
  91:                 "<TR class=ms-viewheadertr><TH class=\"ms-vh2-nofilter ms-vh2-gridview\" scope=col>Scope</TH><TH class=\"ms-vh2-nofilter ms-vh2-gridview\" scope=col>Parent</TH>" +
  92:                 "<TH class=\"ms-vh2-nofilter ms-vh2-gridview\" scope=col>Feature ID</TH><TH class=\"ms-vh2-nofilter ms-vh2-gridview\" scope=col>Feature name</TH>" +
  93:                 "<TH class=\"ms-vh2-nofilter ms-vh2-gridview\" scope=col>Current version</TH><TH class=\"ms-vh2-nofilter ms-vh2-gridview\" scope=col>Upgrade?</TH></TR>");
  94:         }
  95:  
  96:         private void writeTableRows(StringBuilder builder, List<SPFeature> featuresToProcess)
  97:         {
  98:             foreach (SPFeature feature in featuresToProcess)
  99:             {
 100:                 string parent = FeatureManager.GetFeatureParent(feature);
 101:  
 102:                 builder.AppendFormat("<TR style=\"BACKGROUND-COLOR: #dddddd\"><TD class=ms-vb2><SPAN>{0}</SPAN></TD>" +
 103:                     "<TD class=ms-vb2><SPAN>{1}</SPAN></TD>" +
 104:                     "<TD class=ms-vb2><SPAN>{2}</SPAN></TD>" +
 105:                     "<TD class=ms-vb2><SPAN>{3}</SPAN></TD>" +
 106:                     "<TD class=ms-vb2><SPAN>{4}</SPAN></TD>" +
 107:                     "<TD class=ms-vb2><INPUT id=\"{5}\" CHECKED type=checkbox class=\"chkUpgrade\"></TD></TR>",
 108:                         feature.FeatureDefinitionScope,
 109:                         parent,
 110:                         feature.DefinitionId,
 111:                         feature.Definition.DisplayName,
 112:                         feature.Version,
 113:                         new FeatureDetails()
 114:                         {
 115:                             FeatureScope = feature.Definition.Scope,
 116:                             FeatureID = feature.DefinitionId,
 117:                             ParentID = new Guid(FeatureManager.GetFeatureParentId(feature).ToString()),
 118:                             GrandParentID = new Guid(FeatureManager.GetFeatureGrandParentId(feature).ToString()),
 119:                             FeatureName = feature.Definition.DisplayName,
 120:                             ParentString = parent
 121:                         }.ToString()
 122:                 );
 123:             }
 124:         }
 125:  
 126:         private void writeTableFooter(StringBuilder builder)
 127:         {
 128:             builder.AppendFormat("</TBODY></TABLE>");
 129:         }
 130:  
 131:         #endregion
 132:  
 133:         public void ProcessRequest(HttpContext context)
 134:         {
 135:             SPSecurity.CatchAccessDeniedException = false;
 136:             try
 137:             {
 138:                 string operation = context.Request.QueryString[qsOperation];
 139:  
 140:  
 141:                 if (operation == "QueryFeatures")
 142:                 {
 143:                     // collect info..
 144:                     string scope = context.Request.QueryString[qsScope];
 145:                     bool upgradeOnly = true;
 146:                     bool.TryParse(context.Request.QueryString[qsUpgradeOnly], out upgradeOnly);
 147:                     SPFeatureScope upgradeScope = (SPFeatureScope)Enum.Parse(typeof(SPFeatureScope), scope);
 148:                     string parentUrl = context.Request.QueryString[qsParentUrl];
 149:  
 150:                     // get data..
 151:                     List<SPFeature> featuresToProcess = getFeaturesForUpgrade(upgradeScope, upgradeOnly, parentUrl);
 152:  
 153:                     // build object to send to client..
 154:                     FeatureQueryResults queryResultsForClient = new FeatureQueryResults
 155:                     {
 156:                         ResultCount = featuresToProcess.Count,
 157:                         Html = buildQueryFeaturesResponse(context, featuresToProcess)
 158:                     };
 159:  
 160:                     // serialize and send..
 161:                     JavaScriptSerializer serializer = new JavaScriptSerializer();
 162:                     StringBuilder sbJsonResults = new StringBuilder();
 163:                     serializer.Serialize(queryResultsForClient, sbJsonResults);
 164:  
 165:                     context.Response.Clear();


 166:                     context.Response.ContentType = "application/json; charset=utf-8";
 167:                     context.Response.Write(sbJsonResults.ToString());
 168:                 }
 169:                 if (operation == "UpgradeFeatures")
 170:                 {
 171:                     // collect info..
 172:                     string features = context.Request.QueryString[qsFeaturesForUpgrade];
 173:                     string[] featuresArray = features.Split(',');
 174:  
 175:                     // get data..
 176:                     Dictionary<FeatureDetails, IEnumerable<Exception>> upgradeResults = null;
 177:                     List<FeatureDetails> upgradeIdentifiers = new List<FeatureDetails>();
 178:                     foreach (string featureId in featuresArray)
 179:                     {
 180:                         upgradeIdentifiers.Add(FeatureDetails.CreateFromString(featureId));
 181:                     }
 182:  
 183:                     SPSecurity.RunWithElevatedPrivileges(delegate()
 184:                     {
 185:                         upgradeResults = FeatureManager.UpgradeFeatures(upgradeIdentifiers);
 186:                     });
 187:  
 188:                     // build object to send to client..
 189:                     FeatureUpgradeResults upgradeResultsForClient = new FeatureUpgradeResults();
 190:                     upgradeResultsForClient.HasErrors = FeatureManager.CollectionHasErrors(upgradeResults);
 191:                     upgradeResultsForClient.Results = getSimplifiedResults(upgradeResults);
 192:  
 193:                     // serialize and send..
 194:                     JavaScriptSerializer serializer = new JavaScriptSerializer();
 195:                     StringBuilder sbJsonResults = new StringBuilder();
 196:                     serializer.Serialize(upgradeResultsForClient, sbJsonResults);
 197:  
 198:                     context.Response.Clear();
 199:                     context.Response.ContentType = "application/json; charset=utf-8";
 200:                     context.Response.Write(sbJsonResults.ToString());
 201:                 } 
 202:  
 203:             }
 204:             catch (Exception e)
 205:             {
 206:                 context.Response.Write(string.Format("Sorry, an error occurred on your last action:<br /><br />{0}", e));
 207:             }
 208:         }
 209:  
 210:         private void writeDebugInfo(HttpContext context)
 211:         {
 212:             string operation = context.Request.QueryString[qsOperation];
 213:             string upgradeOnly = context.Request.QueryString[qsUpgradeOnly];
 214:             string scope = context.Request.QueryString[qsScope];
 215:             string parentUrl = context.Request.QueryString[qsParentUrl];
 216:  
 217:             string debug = string.Format("Operation = {0}<br/>, Upgrade only = {1}<br/>, Scope = {2}<br/>, Parent URL = {3}..<br/><br/>",
 218:                 operation, upgradeOnly, scope, parentUrl);
 219:  
 220:             context.Response.Write(debug);
 221:         }
 222:  
 223:         /// <summary>
 224:         /// You will need to configure this handler in the web.config file of your 
 225:         /// web and register it with IIS before being able to use it. For more information
 226:         /// see the following link: http://go.microsoft.com/?linkid=8101007
 227:         /// </summary>
 228:         #region IHttpHandler Members
 229:  
 230:         public bool IsReusable
 231:         {
 232:             // Return false in case your Managed Handler cannot be reused for another request.
 233:             // Usually this would be false in case you have some state information preserved per request.
 234:             get { return true; }
 235:         }
 236:  
 237:         #endregion
 238:     }
 239: }

Since I now had more JavaScript/jQuery in the AJAX version, it seemed a logical step to take advantage of some of SharePoint 2010’s JS frameworks – for example, my confirmation messages were now easy to put in the SP2010 status bar:

FeatureUpgradeKitAjax

Although I ended up with a fully-functioning app, I decided not to go ahead and implement the final bits and pieces to ‘production-ize’ these changes (into the main Codeplex release). It didn’t feel like there was much more to do, but ultimately the exercise was about me learning AJAX techniques rather than about this specific tool, and realistically there isn’t a ton of added value for AJAX in this particular case. No doubt this is partly because most users of this tool would expect the operation to take a bit of time, and therefore a postback is pretty acceptable. In case you’re interested, this was my list of observations or things I’d want to iron out:

  • Replace hardcoded HTML in HTTP handler with jQuery templates (which weren’t actually released when I did the work)
  • Consider presentation speed of results – currently there is an SP.UI.Notify message saying something like ‘Feature upgrade started..’, but unless lots of processing is required to upgrade the Features things are so quick that it shows up at the same time as the results are shown!
  • Some refactoring of the HTTP handler and jQuery would be nice

Download the files

For anyone interested, I posted two VS solutions (before and after AJAX). In fact they are a specific release of the main Codeplex project, available here. If you take a look and run into issues or have questions, please post here or on Codeplex.

Wrap up

So this concludes my SharePoint and AJAX series, I hope it was in some way useful. Although this post has perhaps shown that AJAX isn’t necessary everywhere, recently I noticed a couple of experienced SharePoint folks complaining on Twitter about the postback hell which is the ‘Manage User Profile Properties’ page in SharePoint 2007/2010 Central Administration. This is the page I used as an example in my AJAX talk and referred to in the 1st article – if you have any doubts about why you should build your apps, web parts and page controls with AJAX these days, feel free to go to that page and reorder a bunch of properties! I genuinely believe it’s an important approach for any developer these days, and you’ll never look back once you’ve done your first one. Happy coding!