STSDEV Theme Solution Generator

SharePoint for End Users just put up up a post about custom themes that echoed the same process that has been repeated several times before:

  1. Log into your SharePoint server
  2. Copy an existing theme
  3. Rename OLDTHEME.INF to NEWTHEME.INF and inside replace OldTheme with NewTheme
  4. Modify TEMPLATE\LAYOUTS\1033\SPTHEMES.XML
  5. Modify theme.css, images, etc.

I like this article in particular because it points out to the “end user” the un/ghosting caveat of SPD customizations that administrators despise and most developers learn on day 1. But it still advises the changes be made directly on the file system, with no mention of how that should be done. Sure it’s an end user audience, but modifying the hive should bump it squarely out of beginner territory.

That said, one of the comments got me thinking:

I have just one question. Is there a way to deploy custom themes as a solution?

Well, we developers all know the answer is yes. I’ve previously discussed updating SPTHEMES.XML, and there are several tools (WSPBuilder, STSDEV, VSeWSS (in theory)…) that can handle “solutionizing” the theme resources, but there has to be a better way. And through STSDEV‘s solution provider interface, there is!

Imaginiff

Suppose getting started on a new theme were as simple as this:

  1. Create Solution
    STS Theme Generator - Create Solution
  2. Pick Theme Template
  3. Enter Theme Details
  4. Solutionized!

The solution provider copies the existing theme, renames the .INF, does the search and replace, and even builds a farm-scoped feature complete with receiver to update SPTHEMES.XML. The current STSDEV release (1.3) doesn’t populate the feature receiver assembly, but after adding that to feature.xml the solution should be ready to customize and deploy.

I need to clean up the code a bit before I put it on CodePlex, but in the meantime you can download the binary here: STSDEV 1.3a with Theme Generator. If you’re new to STSDEV, check out Ted‘s Tutorial Screencasts (scroll down) to get started. And if you have any problems or feature requests, please let me know. One idea that I’m already planning to implement is an option to convert a theme to use Heather Solomon‘s @import technique.

Update 12/11/2008: An updated release is available here: STSDEV 1.3b with Theme Generator

SPDataSource Mystery Modes: Webs & ListOfLists

Chris O’Brien is right: SPDataSource is quite handy. However, its documentation leaves something to be desired. In particular, there aren’t any examples (that I can find) of how to use the Webs and ListOfLists SPDataSourceModes. It turns out that the result sets map to a subset of properties (both internal and public) of SPWeb and SPList, respectively—but prepended with “__sp”. An easy way to see all available fields is to bind the data source to an asp:GridView with auto-generated columns:

<WSS:SPDataSource runat="server" ID="dsWebs" DataSourceMode="Webs" />
<asp:GridView runat="server" ID="grdWebs" DataSourceID="dsWebs" AutoGenerateColumns="True">
    <RowStyle VerticalAlign="Top" />
</asp:GridView>
<WSS:SPDataSource runat="server" ID="dsLists" DataSourceMode="ListOfLists" />
<asp:GridView runat="server" ID="grdLists" DataSourceID="dsLists" AutoGenerateColumns="True">
    <RowStyle VerticalAlign="Top" />
</asp:GridView>

For easy reference, I’ve put together a complete list of fields here:
SPDataSource Fields for Webs & ListsOfLists

Note that SharePoint Designer’s live preview of the grid shows extra columns (for Webs: __spAlerts, __spAllProperties, __spAllUsers, etc) that aren’t included in the rendering outside of Designer.

SPDataSourceMode.Webs Example

A list of fields is all well and good, but what can we do with it? Suppose we want an easy way to access our favorite settings pages for our subwebs. A contrived example, perhaps, but it will serve its purpose. We start with our data source:

<WSS:SPDataSource runat="server" ID="dsWebs"
  DataSourceMode="Webs" IncludeHidden="true"/>

Next, we’ll define a MenuTemplate of the site settings shortcuts we want available. %URL% is a token we’ll define in our SPMenuField.

<WSS:MenuTemplate runat="server" ID="WebMenu" CompactMode="true">
  <WSS:MenuItemTemplate runat="server" Text="Create"
    ClientOnClickNavigateUrl="%URL%/_layouts/create.aspx" />
  <WSS:MenuItemTemplate runat="server" Text="Site Settings"
    ClientOnClickNavigateUrl="%URL%/_layouts/settings.aspx" />
  <WSS:MenuSeparatorTemplate runat="server" />
  <WSS:MenuItemTemplate runat="server" Text="People and groups"
    ClientOnClickNavigateUrl="%URL%/_layouts/people.aspx" />
  <WSS:MenuItemTemplate runat="server" Text="Advanced permissions"
    ClientOnClickNavigateUrl="%URL%/_layouts/user.aspx" />
  <WSS:MenuSeparatorTemplate runat="server" />
  <WSS:MenuItemTemplate runat="server" Text="Site libraries and lists"
    ClientOnClickNavigateUrl="%URL%/_layouts/mcontent.aspx" />
  <WSS:MenuItemTemplate runat="server" Text="Sites and workspaces"
    ClientOnClickNavigateUrl="%URL%/_layouts/mngsubwebs.aspx" />
  <WSS:MenuItemTemplate runat="server" Text="Site features"
    ClientOnClickNavigateUrl="%URL%/_layouts/ManageFeatures.aspx" />
 <WSS:MenuItemTemplate runat="server" Text="Delete this site"
    ClientOnClickNavigateUrl="%URL%/_layouts/deleteweb.aspx" />
</WSS:MenuTemplate>

And finally, a simple SPGridView with a link and menu for our site, with an extra column just for good measure:

<WSS:SPGridView runat="server" ID="spGrdWebs"
  DataSourceID="dsWebs" AutoGenerateColumns="false">
  <Columns>
    <WSS:SPMenuField HeaderText="Site Title"
      NavigateUrlFields="__spDefaultUrl"
      NavigateUrlFormat="{0}"
      MenuTemplateId="WebMenu"
      TokenNameAndValueFields="URL=__spUrl,ID=__spID"
      TextFields="__spTitle" />
    <WSS:SPBoundField HeaderText="Site Created"
      DataField="__spCreated" />
  </Columns>
</WSS:SPGridView>

And our final result will look something like this:

So we’ve seen how to use SPDataSource in Webs mode, plus we have a code-free example of the often-overlooked SPMenuField. For what else could the Webs and ListOfLists modes be used?

Quick Tip: Local Central Admin SPWebApplication

I’ve seen several incoming searches like “SPWebApplication for Central Admin”, so hopefully this will provide a quick answer:

SPAdministrationWebApplication.Local

Which really just returns:

SPAdministrationWebApplication.GetInstanceLocalToFarm(SPFarm.Local);

And a quick example in PowerShell for good measure:

PS C:\> [system.reflection.assembly]::LoadWithPartialName("Microsoft.SharePoint") > $null
PS C:\> $ca = [Microsoft.SharePoint.Administration.SPAdministrationWebApplication]::Local
PS C:\> $ca.DefaultServerComment
SharePoint Central Administration v3
PS C:\> $ca.AdministrativeTasks.Items | select Title -first 3

Title
-----
Add anti-virus protection
READ FIRST - Click this link for deployment instructions
Configure Workflow Settings

Hope this helps!

Is SPList.ParentWeb a Leak?

As previously discussed, doing the “right thing” with our SPSite and SPWeb references’ disposability is way harder than it should be. And even worse is the fact that the experts don’t seem to agree how that should be done. Now I’m definitely not an expert, and I’m not even sure who’s moderating the debate, but I’m intrigued so I guess I’ll invite myself anyway.

First, the code in question, from a useful STSADM extension by Gary Lapointe:

  lvw = new ListViewWebPart();
  SPList list = Utilities.GetListFromViewUrl(listUrl);
  if (list == null)
    throw new ArgumentException("List not found.");

  lvw.ListName = list.ID.ToString("B").ToUpperInvariant();
  lvw.TitleUrl = list.DefaultViewUrl;
  lvw.WebId = list.ParentWeb.ID;

The question: Does list.ParentWeb need to be disposed? My first instinct was that it does, based on this quote from Roger Lamb’s dispose patterns:

When using the SPList.BreakRoleInheritance() method a internal call to ParentWeb property is called and must be disposed by the caller.

But upon further consideration, is that even true? Well, let’s reflect SPList.ParentWeb to see what we’re dealing with:

    if (!SPUtility.StsCompareStrings(this.m_Lists.Web.ServerRelativeUrl, this.ParentWebUrl)) {
      if (this.m_parentWeb == null) {
        this.m_parentWeb = this.m_Lists.Web.Site.OpenWeb(this.ParentWebUrl);
      }
    }
    else {
      return this.m_Lists.Web;
    }
    return this.m_parentWeb;

Or rewritten for clarity:

    if (SPUtility.StsCompareStrings(this.m_Lists.Web.ServerRelativeUrl, this.ParentWebUrl))
        return this.m_Lists.Web;

    if (this.m_parentWeb == null)
        this.m_parentWeb = this.m_Lists.Web.Site.OpenWeb(this.ParentWebUrl);
    return this.m_parentWeb;

So if the list’s parent collection matches its ParentWebUrl, it uses the collection’s Web; if not it returns m_parentWeb, which Reflector verifies is only set by the preceding call to Site.OpenWeb(). As far as I can tell, every call to OpenWeb requires a matching call to Dispose, and as SPList isn’t IDisposable that responsibility would seem to fall to the developer. So we should Dispose, right? Well, maybe… We can’t forget about the first if: OpenWeb is only called if the list isn’t in its collection’s web. (Bonus question: how could that happen?)

So it seems the most common scenario is that SPList.ParentWeb will just defer to SPListCollection.Web, which returns SPListCollection.m_web. And m_web is only set by an internal SPListCollection constructor, used by SPWeb.Lists:

    if (this.m_Lists == null)
      this.m_Lists = new SPListCollection(this);
    return this.m_Lists;

So with all that in mind, consider the following code:

    SPWeb web = SPContext.Current.Web;
    SPList list = web.Lists["ListName"];
    list.BreakRoleInheritance();
    list.ParentWeb.Dispose(); // Best practice?

BreakRoleInheritance does indeed have an internal reference to ParentWeb (hidden in SPList.SecurableObjectImpl), but in this case ParentWeb will be web, which is our context web and should not be disposed!

So back to Gary’s example. Before we can decide how to handle list.ParentWeb, we should check out the helper that created list for us:

    internal static SPList GetListFromViewUrl(string url) {
      using (SPSite site = new SPSite(url))
      using (SPWeb web = site.OpenWeb()) {
        return GetListFromViewUrl(web, url);
      }
    }

Since the SPWeb was opened based on the list URL, it should be safe to assume that ParentWeb won’t need to call OpenWeb again and will instead use Lists.Web, which will be a reference to the original webwhich is disposed (by using) in the helper. So list.ParentWeb indeed doesn’t need to be disposed because it’s already been disposed. In practice we can probably get away with it (in this case), but there are no guarantees that a disposed SPWeb will be in a consistent state when we get around to using it.

Though we’ve answered the original question of disposal, we should really eliminate the post-disposal reference. My preference is to refactor the ListViewWebPart code into helpers modeled after Gary’s existing GetListFromViewUrl methods:

    internal static ListViewWebPart GetListViewWebPartFromViewUrl(string url) {
      using (SPSite site = new SPSite(url))
      using (SPWeb web = site.OpenWeb()) {
        return GetListViewWebPartFromViewUrl(web, url);
      }
    }

    internal static ListViewWebPart GetListViewWebPartFromViewUrl(SPWeb web, string url) {
      ListViewWebPart lvw;
      lvw = new ListViewWebPart();
      SPList list = GetListFromViewUrl(web, url);
      if (list == null)
        throw new ArgumentException("List not found.");

      lvw.ListName = list.ID.ToString("B").ToUpperInvariant();
      lvw.TitleUrl = list.DefaultViewUrl;
      lvw.WebId = web.ID;
      return lvw;
    }

Which greatly simplifies the original code and eliminates the ParentWeb risk:

  lvw = Utilities.GetListViewWebPartFromViewUrl(listUrl);

Conclusions

So back again to the original question: Does SPList.ParentWeb need to be disposed? The answer seems to be a qualified “probably not”. But even if we’re not leaking an SPWeb reference, more concerning is the potential for ParentWeb to return an object that has already been disposed. If nothing else, be careful with ParentWeb on lists returned from helpers that might have cleaned up prematurely.

PowerShellASP Applied: JSON Factory

Now that we can use PowerShellASP with SharePoint, what can we do with it? Well without master page or web part support, we can’t easily integrate PoShASP content directly into the SharePoint experience. Fortunately, we can leverage PowerShell in other ways. An easy example is generating JSON, perhaps from the PowerShell standby Get-Process:

<% $Response.ContentType = "application/json; charset=utf-8" %>
<% $Response.Cache.SetCacheability([System.Web.HttpCacheability]'NoCache') %>
<% $Response.Cache.SetExpires([DateTime]::MinValue) %>
<% if( !($n = $Request.QueryString["n"] -as [int]) ) { $n = 10 } %>
{"processes":[
  <% [string]::Join(",`n  ",
  (get-process | sort ws -desc | select -first $n | foreach {
  "{'ID':$($_.ID),'Name':'$($_.Name)','WS':$($_.ws)}" })) %>
]}

After we set the JSON ContentType and cache policy, we try to fetch an “n” query parameter. If n is missing or not a number, we default to 10. Now we can start building our JSON object. To build our array, we use System.String‘s static Join method (the backtick is PowerShell’s escape character, so `n is a newline). Join‘s string[] parameter is provided by a PowerShell pipeline that fetches our system processes, sorts by working set size, selects the top $n and then iterates through those process objects, returning a string of JSON with our desired properties. Note also that PoShASP seems to require an empty line at the end of the file, otherwise “]}” is ignored. Our output will look something like this:

{"processes":[
  {'ID':1672,'Name':'sqlservr','WS':506028032},
  {'ID':3296,'Name':'devenv','WS':230555648},
  {'ID':4720,'Name':'w3wp','WS':181100544},
  {'ID':376,'Name':'services','WS':121995264},
  {'ID':3372,'Name':'w3wp','WS':102522880}
]}

Let’s save our JSON endpoint as %12%\TEMPLATE\LAYOUTS\Get-Process.json.ps1x, which we can test by visiting http://server/_layouts/Get-Process.json.ps1x?n=5. Now it’s AJAX time! Since not everyone can use the ASP.NET AJAX Extensions, we’ll just do it the old-fashioned way. Starting with a Content Editor Web Part on a page of your choice, set the source to the following:

<div id="d_procs">
<img src="/_layouts/images/GEARS_AN.GIF"
  alt="Loading..." align="center" />
</div>
<script>
function getXmlHttpRequestObject() {
 if (window.XMLHttpRequest) {
  return new XMLHttpRequest();
 } else if(window.ActiveXObject) {
  return new ActiveXObject("Microsoft.XMLHTTP");
 } else {
  document.getElementById('d_procs').innerHTML =
  'Cound not create XmlHttpRequest Object.';
 }
}

var r = getXmlHttpRequestObject();
var c = 0;
var mTimer;

function getProcs() {
 if (r.readyState == 4 || r.readyState == 0) {
  r.open("GET", '/_layouts/Get-Process.json.ps1x?n=5', true);
  r.onreadystatechange = handleReceiveProcs;
  r.send(null);
 }
}

function handleReceiveProcs() {
 if (r.readyState == 4) {
  var d = document.getElementById('d_procs');
  var response = eval("(" + r.responseText + ")");
  var p = response.processes;
  var t = "<table width=\"100%\">";
  t += "<tr><th>ID</th><th>Name</th><th>WS(MB)</th></tr>";
  for(i=0;i < p.length; i++) {
   t += '<tr><td>'+p[i]['ID']+'</td>';
   t += '<td>'+p[i]['Name']+'</td/>';
   t += '<td align=\"right\">';
   t += (p[i]['WS']/(1024*1024.0)).toFixed(2);
   t += '</td/></tr>';
  }
  t += "</table><p>Refresh count: "+(++c)+"</p>";
  d.innerHTML = t;
  mTimer = setTimeout('getProcs();', 3000); // 3-sec refresh
 }
}

_spBodyOnLoadFunctionNames.push('getProcs');
</script>

I haven’t put much effort into cleaning up the code, but it does the job (in FF2 and IE7, at least):

With PowerShell’s ease of development and specialized adapters, this is just the tip of the iceberg. What other real-time data would you find useful?

PowerShellASP with SharePoint: Scripts in Content Database

In my last post, I mentioned that PowerShellASP doesn’t work for files stored in a SharePoint content database. It wasn’t hard to track down why this is the case, so my next step was to attempt a fix.

First, a recap of the relevant code. In PSHandler.ProcessRequest(HttpContext), the entry point for the handler, we have:

    string filename = A_0.Request.MapPath(A_0.Request.FilePath);
    ...
    this.a(filename);

MapPath doesn’t know to do anything special with content files, so it returns a useless path which is then passed to PSHandler.a(String A_0) and used here:

    using (StreamReader reader = new StreamReader(A_0)) {
      ...

Rather than modify the existing handler (much), I propose creating a SharePoint-specific handler that inherits from PSHandler. Before we can do that, we need to refactor the original slightly to facilitate the extension.

The biggest change is extracting the contents of the using(StreamReader) block into a new protected method that accepts a StreamReader:

    using (StreamReader reader = new StreamReader(A_0)) {
      this.a(reader);
    }

We should also change the private HttpContext field to protected, which I will call _a for our example. Now we have everything we need to extend PSHandler:

  public class SPPSHandler : PSHandler, IHttpHandler {
    public new void ProcessRequest(HttpContext context) {
      base._a = context;

      HttpRequest request = context.Request;
      string filePath = request.FilePath;
      string mappedPath = request.MapPath(filePath);
      if (File.Exists(mappedPath))
        using (StreamReader sr = new StreamReader(mappedPath)) {
          base.a(sr);
          return;
        }

      SPContext currentContext = SPContext.Current;
      if (currentContext != null) {
        SPWeb web = currentContext.Web;
        SPFile file = null;

        if (web != null && (file = web.GetFile(filePath)) != null && file.Exists)
          using (StreamReader sr = new StreamReader(file.OpenBinaryStream())) {
            base.a(sr);
            return;
          }
      }

      // Quoth the server...
      context.Response.StatusCode = 404;
    }
  }

A theoretical subclass is great and all, but does it work? Well we can’t change the original assembly, and I don’t feel like messing with reflection, so let’s build a mock PSHandler instead:

  public class PSHandler : IHttpHandler {
    protected HttpContext _a;

    public bool IsReusable {
      get { return true; }
    }

    public void ProcessRequest(HttpContext A_0) {
      this._a = A_0;
      A_0.Response.Write("PSHandler"); // Shouldn't see this
    }

    protected void a(StreamReader reader) {
      TextWriter o = _a.Response.Output;
      o.Write("<html><body><pre>");
      o.Write(SPEncode.HtmlEncodePreserveSpaces(reader.ReadToEnd().ToUpper()));
      o.Write("</pre></body></html>");
    }
  }

After adjusting the feature receiver to reference our new handler, we see that it does indeed behave as expected:
PowerShellASP in SharePoint - Extended Handler
I haven’t tested extensively, but it’s a start. I doubt the PoShASP team would bother with these changes just for me, so if you’re at all interested leave a comment or at least contact them.

PowerShellASP with SharePoint

PowerShellASP was announced earlier this week, and naturally my first thought was “Does it work with SharePoint?” It turns out that it does, but only for paths mapped to the file system (_layouts, _admin, etc). I’m hoping the authors will consider making a SharePoint extension of the handler to support files stored in the content database as well (more on that later). Just think…writing PowerShell in SharePoint Designer! What could be better?!

Solutionizing PoShASP

Of course you could just follow the provided installation instructions by hand, but what if we wanted to use a WSP?

Step 1: Install PowerShell 1.0

PowerShell is included on Windows Server 2008; for other flavors of Windows, download here.

Step 2: Web.Config Changes

PoShASP requires adding an HttpHandler to web.config. The preferred way to do this is through a WebApplication-scoped feature receiver. The code would look something like this:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;

namespace Solutionizing.PowerShellASP {
  class WebApplicationFeatureReceiver : SPFeatureReceiver {
    private const string MODS_OWNER = "PowerShellASP";
    private const string PS_VERB = "*";
    private const string PS_PATH = "*.ps1x";
    private const string PS_TYPE = "PowerShellToys.PowerShellASP.PSHandler, PowerShellToys.PowerShellASP";
    private const string MOD_PATH  = "configuration/system.web/httpHandlers";
    private const string MOD_NAME  = @"add[@path=""{0}""]";
    private const string MOD_VALUE = @"<add verb=""{0}"" path=""{1}"" type=""{2}""/>";

    private SPWebConfigModification AddHttpHandler(string verb, string path, string type) {
      SPWebConfigModification mod = new SPWebConfigModification(string.Format(MOD_NAME, path), MOD_PATH);
      mod.Value = String.Format(MOD_VALUE, verb, path, type);
      mod.Owner = MODS_OWNER;
      mod.Sequence = 0;
      mod.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
      return mod;
    }

    public override void FeatureActivated(SPFeatureReceiverProperties properties) {
      SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
      webApp.WebConfigModifications.Add(AddHttpHandler(PS_VERB, PS_PATH, PS_TYPE));
      webApp.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
    }

    public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {
      SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
      Collection<SPWebConfigModification> mods = webApp.WebConfigModifications;

      int startCount = mods.Count;
      for (int c = startCount - 1; c >= 0; c--) {
        SPWebConfigModification mod = mods[c];
        if (mod.Owner == MODS_OWNER)
          mods.Remove(mod);
      }

      if (startCount > mods.Count) {
        webApp.Update();
        webApp.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
      }
    }

    public override void FeatureInstalled(SPFeatureReceiverProperties properties) { }
    public override void FeatureUninstalling(SPFeatureReceiverProperties properties) { }
  }
}

And we need a feature for the receiver. (Download image here.)
PowerShell Logo

<Feature Id="316F5804-C11B-4E4B-9CD3-E6BD6801091A"
         Title="PowerShellASP"
         Description="Installs PowerShellASP - http://www.powershelltoys.com/"
         Scope="WebApplication"
         Version="1.0.0.0"
         ImageUrl="PowerShellASP/PoSh_60x45.jpg"
         ImageUrlAltText="PowerShell"
         ReceiverAssembly="Solutionizing.PowerShellASP, ..."
         ReceiverClass="Solutionizing.PowerShellASP.WebApplicationFeatureReceiver"
         xmlns="http://schemas.microsoft.com/sharepoint/" />

Step 3: Test Script

Before we finish the package, let’s add a sample script in LAYOUTS – test.ps1x:

<html>
<body>
<% $s = new-object Microsoft.SharePoint.SPSite($Request.Url) %>
<% $raw = $Request.RawUrl %>
<% $w = $s.OpenWeb($raw.Substring(0, $raw.IndexOf($Request.Path))) %>
<h1>Hidden Lists in <a href="<%=$w.Url %>"><%=$w.Title %></a></h1>
<table width="100%">
<tr><th>List Title</th><th>Items<tr></th></tr>
<% $w.Lists | ?{$_.Hidden -eq $true} |
   sort @{expression="ItemCount";Ascending=$true},@{expression="Title";Descending=$true} | %{ %>
<tr>
  <td><a href="<%= $w.Url %><%= $_.DefaultViewUrl %>"><%= $_.Title %></a></td>
  <td><%= $_.ItemCount %></td>
</tr>
<tr><td colspan="2"><p style="overflow: auto; height: 8em">
<%= [Microsoft.SharePoint.Utilities.SPEncode]::HtmlEncode($_.PropertiesXml) %>
</p></td></tr>
<% } %>
</table>
<pre>
<% $w %>
<% $s %>
<% $Request %>
</pre>
</body>
</html>

This contrived example fetches the current SPSite and SPWeb and then outputs a table of the hidden lists in the current site with a few properties. It also dumps the default PowerShell view of the current SPWeb, SPSite and HttpRequest objects. It’s not exactly pretty, but it sufficiently demonstrates a few key concepts of PoShASP.

Step 4: Deploy DLL

The final piece is the assembly, which needs to go in the web application’s bin directory. Our solution brings everything together:

<Solution SolutionId="5C594B30-9AE5-4910-8DA2-1D7BA622FACC"
          ResetWebServer="True"
          xmlns="http://schemas.microsoft.com/sharepoint/">
  <FeatureManifests>
    <FeatureManifest Location="PowerShellASP\feature.xml" />
  </FeatureManifests>
  <TemplateFiles>
    <TemplateFile Location="IMAGES\PowerShellASP\PoSh_60x45.jpg" />
    <TemplateFile Location="LAYOUTS\PowerShellASP\test.ps1x" />
  </TemplateFiles>
  <Assemblies>
    <Assembly Location="Solutionizing.PowerShellASP.dll"
              DeploymentTarget="GlobalAssemblyCache" />
    <Assembly Location="PowerShellToys.PowerShellASP.dll"
              DeploymentTarget="WebApplication" />
  </Assemblies>
</Solution>

Step 5: Deploy

Now that our solution is complete, we can install and deploy the WSP. Note that the package contains a resource—the PoShASP DLL—scoped for a web application, so deploy accordingly. Once the solution is deployed, the feature needs to be activated through Central Admin or stsadm (or PowerShell ;). If all goes according to plan, the application’s bin directory will contain PowerShellToys.PowerShellASP.dll and web.config will have an HttpHandler for .ps1x files. Now open your browser to http://yourapp/_layouts/PowerShellASP/test.ps1x and see what we have. You can also try http://yourapp/SubSite/_layouts/PowerShellASP/test.ps1x to see the change in site context. (Screenshot was taken before I rolled test.ps1x into my test solution.)

From Content Database

Earlier I mentioned that the handler doesn’t work for scripts stored in the content database. The easiest way to test this is to upload a .ps1x file to a document library. Attempting to open the file yields a lovely exception. So where is this exceptional path coming from? Well the stack trace gives us a good place to start:

   System.IO.StreamReader..ctor(String path) +112
   PowerShellToys.PowerShellASP.PSHandler.a(String A_0) +75

Cue Reflector. In PSHandler.a(String A_0) we find the following:

    using (StreamReader reader = new StreamReader(A_0)) {
      ...

And A_0 comes from the implementation of IHttpHandler.ProcessRequest(HttpContext):

    string filename = A_0.Request.MapPath(A_0.Request.FilePath);
    ...
    this.a(filename);

Since Docs/Documents/Get-Process.ps1x doesn’t map to anything special like _layouts, IIS just maps it to the local root of the site. We know this won’t work, but how can we let the handler in on our little secret? In my next post I’ll go over some code that could get us to that point.

With or without the content database limitation, how would you use PowerShellASP with SharePoint?

SPSite/SPWeb Leaks Revisited

A while back I posted a rather clumsy technique to mitigate an SPWeb leak discussed here. I knew there had to be a better way, and Rob Garrett‘s use of delegates seems to have potential.

But first, I should point out a rather subtle leak in Chris’s and my code:

        list = currentContext.Site.AllWebs["MyWeb"].Lists["MyList"];

See it? How about now:

        SPWeb web = currentContext.Site.AllWebs["MyWeb"];
        list = web.Lists["MyList"];

The context SPSite shouldn’t be disposed, but AllWebs returns an SPWeb that should (according to Roger Lamb):

        using (SPWeb web = currentContext.Site.AllWebs["MyWeb"]) {
          list = web.Lists["MyList"];
        }

That Chris and I (and our readers) can overlook this in posts about SPWeb leaks is a testament to how tricky this stuff can be.

Super Delegates?

So how would Rob’s technique be used to DoSomething? Well none of his helpers take advantage of SPContext, so first let’s add a helper for that:

public static void GetContextWebByTitle(string title, Action<SPSite, SPWeb> action) {
  if (String.IsNullOrEmpty(title))
    throw new ArgumentNullException("title");
  if (null == action)
    throw new ArgumentNullException("action");

  SPContext currentContext = SPContext.Current;
  if (null == currentContext)
    throw new SPException("Context is null!");

  SPSite site = currentContext.Site;
  using (SPWeb web = site.AllWebs[title]) {
    if (null == web)
      throw new SPException("Web not found");
    action(site, web);
  }
}

Now we can refactor into a method that matches the delegate:

public void DoSomething(SPSite site, SPWeb web) {
  SPList list = web.Lists["MyList"];

  // do something with list..
  foreach (SPListItem item in list.Items) {
    processItem(item);
  }
}

And our original DoSomething() just determines which helper to call:

public void DoSomething() {
  if (SPContext.Current != null)
    SPHelper.GetContextWebByTitle("MyWeb", DoSomething);
  else
    SPHelper.GetWebByTitle("http://litwaredemo", "MyWeb", DoSomething);
}

Pretty simple. But we’re still thinking a bit too much – couldn’t GetWebByTitle check context for us? Of course:

  if (SPContext.Current != null)
    GetContextWebByTitle(title, action);
  else
    using (var site = new SPSite(url)) {
      ...

So we don’t have to think at all:

public void DoSomething() {
  SPHelper.GetWebByTitle("http://litwaredemo", "MyWeb", DoSomething);
}

I still need to try this technique in some real code, but I like the theory. It certainly makes sense to separate the logic to create and dispose SPSite/SPWeb objects from the code to manipulate them, and it’s even better to standardize that logic. But even with an arsenal of slick helpers, a leaky delegate can take us back to where we started:

public void DoSomething(SPSite site) {
  foreach (SPWeb web in site.AllWebs)
    processWeb(web);
}

Not that this diminishes the value of Rob’s solution, it just reinforces the need for developers to know the disposal patterns even with help.

Faster PowerShell Startup

If you’re a SharePoint developer or admin, you need to learn PowerShell (Neil Iversen had a good intro at DevConn). If you do use PowerShell, you might want to follow the PowerShell Team Blog. If you don’t, you probably missed this post with a script to make PowerShell start faster. Seriously, do it now before you forget (Run as Administrator in Vista/2008):

set-alias ngen @(
  dir -recurse (join-path ${env:\windir} "Microsoft.NET\Framework\") ngen.exe |
    ?{$_.length -gt 0} | sort -descending lastwritetime
)[0].fullname
[appdomain]::currentdomain.getassemblies() | %{ngen $_.location}

Theme-amajig Refactored: Using Feature Properties

In a previous post, I described a feature that would take install and retract modifications to SPTHEMES.XML. Peter Seale suggested providing a method to reapply the changes without a deactivate/activate cycle, specifically for new servers added to a farm. It should be as simple as providing a user interface to call FeatureThemesJob.InstallThemes, but that presents a bit of a problem: InstallThemes expects the name of the themes file, which I declare in the feature receiver. So before we can work on a reapplication interface, let’s move that file name to a more accessible location.

The Revised Feature

A better way to store the theme file name would be as a Feature Property:

<Feature
  Id="E2F8D046-607D-4BB6-93CC-2C04CF04099E"
  Title="SPHOLS Themes"
  Description="Installs SPHOLS and SPHOLSX themes on farm."
  Version="1.0.0.0"
  Scope="Farm"
  ReceiverAssembly="MyBranding, ..."
  ReceiverClass="MyBranding.MyThemesFeatureReceiver"
  xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementFile Location="SPTHEMES.XML" />
  </ElementManifests>
  <Properties>
    <Property Key="Solutionizing:ThemesFile" Value="SPTHEMES.XML" />
  </Properties>
</Feature>

And we can remove references to THEMES_FILE from our receiver:

namespace MyBranding {
  public class MyThemesFeatureReceiver : SPFeatureReceiver {
    public override void FeatureActivated(SPFeatureReceiverProperties properties) {
      if (properties == null)
        throw new ArgumentNullException("properties");
      FeatureThemesJob.InstallThemes(properties.Definition);
    }

    public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {
      if (properties == null)
        throw new ArgumentNullException("properties");
      FeatureThemesJob.DeleteThemes(properties.Definition);
    }

    public override void FeatureInstalled(SPFeatureReceiverProperties properties) { }
    public override void FeatureUninstalling(SPFeatureReceiverProperties properties) { }
  }
}

Note that this code is now completely feature-agnostic, reusable for any themes feature.

FeatureThemesJob

Other than purging the logic to persist _themesFile, which I’ll leave as an exercise for the reader, we just need to update Execute to use our new feature property:

    private const string PROP_THEMES_FILE = "Solutionizing:ThemesFile"
    private const string SPTHEMES_PATH = @"TEMPLATE\LAYOUTS\1033\SPTHEMES.XML";
    public override void Execute(Guid targetInstanceId) {
      SPFeatureDefinition fDef = Farm.FeatureDefinitions[_featureID];
      if (fDef != null) {
        SPFeatureProperty themesFileProp = fDef.Properties[PROP_THEMES_FILE];
        if(themesFileProp == null)
          throw new SPException(string.Format("Feature '{0}' is missing property '{1}'.", fDef.DisplayName, PROP_THEMES_FILE));

        DoMerge(SPUtility.GetGenericSetupPath(SPTHEMES_PATH), Path.Combine(fDef.RootDirectory, themesFileProp.Value));
      }
    }

But since we’re in a refactoring mood, we might as well extract the code to retrieve the themes file path:

    internal const string PROP_THEMES_FILE = "Solutionizing:ThemesFile"
    private const string ERR_FEATURE_NOT_FOUND = "Feature '{0}' not found in farm.";
    private const string ERR_MISSING_PROPERTY = "Feature '{0}' is missing property '{1}'.";
    internal string ThemesFilePath {
      get {
        SPFeatureDefinition fDef = Farm.FeatureDefinitions[_featureID];
        if (fDef == null)
          throw new SPException(string.Format(ERR_FEATURE_NOT_FOUND, _featureID));

        SPFeatureProperty prop = fDef.Properties[PROP_THEMES_FILE];
        if (prop == null)
          throw new SPException(string.Format(ERR_MISSING_PROPERTY, fDef.DisplayName, PROP_THEMES_FILE));

        return Path.Combine(fDef.RootDirectory, prop.Value);
      }
    }

Which makes Execute rather elegant:

    private const string SPTHEMES_PATH = @"TEMPLATE\LAYOUTS\1033\SPTHEMES.XML";
    public override void Execute(Guid targetInstanceId) {
      DoMerge(SPUtility.GetGenericSetupPath(SPTHEMES_PATH), ThemesFilePath);
    }

Now everything we need for the timer job is available from the feature definition. The next step is to build an interface to run the job on demand. Stay tuned!