Developing in SharePoint is much like preparing a large holiday meal. It involves strict ingredients for the recipes, which must be prepared in the correct order using the finite tools you have in your kitchen. No matter how many times you prepare the same meal year-after-year, it is just as challenging the next year around…and there always seems to be that special “request” for something unique. Unless you have a Pee-wee Herman Rube Goldberg machine in your kitchen…the meal requires some planning and structure! (ok maybe not the best analogy, I just wanted to say “Pee-wee Herman”) There are lots of third-party tools out there that help us in the development process, but none seem to incorporate the entire solution development process (including deployment) in a “TRANSPARENT” manner. Meaning, I want to see and know what’s going on and I want to be able to tweak it if I need to. The problem isn’t that a tool can’t be created, it’s the lack of development discipline by SharePoint developers. And creating “features” is really only half of the answer. I won’t try and pawn off my solution as “the best” or “the only”. But it does work, and it structurally makes sense. I also won’t claim all of it as my own idea, so I’ll give credit to those who have paved the path before me (Andrew Connell, Ted Pattison, etc.) For me, the key is not the ability to easily create a web part, it’s the ability for my customers to support the development solution after I leave. And so here’s what works for me: 1. You absolutely HAVE to create SharePoint Solution Package(s) for your development components. Features alone are great, but the deployment and management for them is based on the solution package. (How to create a SharePoint Solution Package) 2. Create your project folder structure to mirror the 12 Hive. (For now, ignore the section on “Deployment”). This will make it MUCH easier to build automation into our project and also easier when it comes time to that pesky *.ddf file. I also create a separate project/folder for each “functional” development component. d:\projects\<Company>\<Company>.SharePoint.Finance\ (where my *.sln file sits) d:\projects\<Company>\<Company>.SharePoint.Finance\<Company>.SharePoint.Finance.Branding\ d:\projects\<Company>\<Company>.SharePoint.Finance\<Company>.SharePoint.Finance.ContentTypes\ d:\projects\<Company>\<Company>.SharePoint.Finance\<Company>.SharePoint.Finance.WebParts\ d:\projects\<Company>\<Company>.SharePoint.Finance\<Company>.SharePoint.Finance.Solution\ 3. Use VERY GOOD naming schemes for your code. I like to use: <Company>.SharePoint.<Application>.<Feature type>.<Feature name> so for instance: Microsoft.SharePoint.Finance.WebParts.MyCustomWebPart class in Microsoft.SharePoint.Finance.WebParts.dll assembly Not only does this leave it easy to find things later, but it adds that all too important structure and repetitiveness to your solution. 4. Build automation. This, I think, is the least performed function during development and where I want to focus on in this post.
1. Copying files between projects 2. Building .wsp solution packages 3. Deploying .wsp solution packages 4. Deploying XML configuration files 5. Logging builds, releases, errors, etc.
I like using user controls in my SharePoint projects. The problem with these (and pages), is that you create them in a Class Library project, which makes them cumbersome to debug. I create all of my user controls in a separate web application project. This makes them easy to test and debug in mock ASPX pages. Then, upon successful build, I use a PowerShell script to copy the files over to my Microsoft.SharePoint.Finance.UserControls project. It looks something like this:
function Copy-SPControls() { ##two parameters needed ## $prjSrc - Path to the source Web Application FOLDER User Controls ## $prjDest - Path to the SharePoint Feature PROJECT for User Controls
param($prjSrc=$(throw "You must pass the path to the source project and folder where User Controls are developed/tested"), $prjDest=$(throw "You must pass the path to the destination project where User Controls are included in a feature")) trap [Exception] { continue;}
$userControlsDest = $prjDest; if($userControlsDest.EndsWith("\") -eq $false) {$userControlsDest = $userControlsDest + "\";} $userControlsDest = $userControlsDest + "TEMPLATE\CONTROLTEMPLATES";
COPY $prjSrc $userControlsDest -RECURSE -FORCE }
Command: powershell.exe import-module –name SPoshMod –force;Copy-SPControls -prjSrc $(ProjectDir)\UserControls –prjDest $(SolutionDir)Company.SharePoint.Finance.UserControls
There are two parts to the WSP solution package. One is getting all of the files I need into one project. The other is calling makecab.exe to make my WSP file for me. Move all files to “Solution” project When I create Visual Studio solution for SharePoint development, I always create one project named <Company>.SharePoint.<Application>.Solution I call this my “Solution” project. This project will store all of my TEMPLATE files, my manifest.xml, and *.ddf file needed to make my WSP file. To get all of the files down there is actually quite easy. When I build the “Solution” project (which is dependent on all other projects), it will call a script and pass in the Visual Studio Solution directory as well as its own project directory. Then for each project under the solution, the script will copy all of the files in the TEMPLATE folder down to the “solution” project’s TEMPLATE folder. It will also get the assembly from the “bin\debug” folder and copy that to an “ASSEMBLIES” folder in the “solution” project. The end result is that I have all of my features, assemblies, etc. ready for me to build a WSP. Here’s what the script looks like:
function Copy-SPFeatureFiles() { ##three parameters needed ## $buildType - Debug|Release right now, "debug" is implemented ## $solFldr - Visual Studio solution folder - all project folders underneath ## $spSolFldr - project that contains the SharePoint Features for building the SharePoint Solution Package (WSP) param($buildType=$(throw "You must pass the build type: debug|release"), $solFldr=$(throw "You must pass the absolute path to the visual studio solution folder"), $spSolFldr=$(throw "You must pass the absolute path to the SharePoint (feature) solution folder")) trap [Exception] { continue;} [System.Reflection.Assembly]::LoadWithPartialName("System.IO"); $path = $solFldr; if($path.EndsWith("\") -eq $false) {$path = $path + "\";} $dest = $spSolFldr; if($dest.EndsWith("\") -eq $false) {$dest = $dest + "\";} $destTemp = $dest.ToLower(); $destTEMPLATE = $dest + "12"; $destASSEMBLIES = $dest + "GAC"; $diRoot = new-object System.IO.DirectoryInfo($dest); $diTEMPLATE = new-object System.IO.DirectoryInfo($destTEMPLATE); if($diTEMPLATE.Exists -eq $true) { $diTEMPLATE.Delete($true); } $diRoot.CreateSubdirectory("12"); $diTEMPLATE = new-object System.IO.DirectoryInfo($destTEMPLATE); $diTEMPLATE.CreateSubdirectory("TEMPLATE"); $diASSEMBLIES = new-object System.IO.DirectoryInfo($destASSEMBLIES); if($diASSEMBLIES.Exists -eq $true) { $diASSEMBLIES.Delete($true); } $diRoot.CreateSubdirectory("GAC"); $diSource = new-object System.IO.DirectoryInfo($path); foreach($dir in $diSource.GetDirectories()) { $dirTemp = $dir.FullName.ToLower(); if($dirTemp.EndsWith("\") -eq $false) {$dirTemp = $dirTemp + "\";} if($dirTemp -ne $destTemp) { $dirSource = new-object System.IO.DirectoryInfo($dir.FullName); $dirSource = $dirTemp + "TEMPLATE\*"; $dirBin = new-object System.IO.DirectoryInfo($dir.FullName); $dirBin = $dirTemp + "bin\*.dll"; $dirBinType = new-object System.IO.DirectoryInfo($dir.FullName); $dirBinType = $dirTemp + "bin\" + $buildType + "\*.dll"; $dirDestAssem = $dest + "GAC"; $dirDestTempl = $dest + "12\TEMPLATE"; copy "$dirSource" "$dirDestTempl" -RECURSE -FORCE copy "$dirBinType" "$dirDestAssem" -RECURSE -FORCE } } }
Here, there is a bit of flexibility in when this script runs. Some developers like to have it run on successful build of the “Solution” project. Some like to also make it only run when built in “Release”. Others just run it as they need. I like to build my projects periodically (without deployment), so I will choose the last option for simplicity. Nothing complicated here, just wrapping the STSADM commands with PowerShell. The nice thing is that the functions are grouped so that I only have to call the Upgrade-Solution function and everything will be updated for me.
function Add-SPSolution() { param($filename=$(throw "You must pass the filename path of the solution")) ##$filename = "e:\projects\customers\<project folder>\SOLUTION\PACKAGE\SharePoint.wsp" trap [Exception] { continue;}
stsadm -o addsolution -filename $filename } function Get-Solutions() { stsadm -o enumsolutions } function Get-SPSolution() { param($name=$(throw "You must pass the name of the solution")) ##$name = SharePoint.wsp $solutions = get-solutions $solutionsXML = [xml]$solutions $solutionsNode = [System.Xml.XmlNode]$solutionsXML $solutionsNode.SelectSingleNode("Solutions/Solution[File='$name']") } function Deploy-SPSolution() { param($name=$(throw "You must pass the name of the solution"),$url=$(throw "You must pass the url to the site for deployment"),[switch]$allowgacdeployment) ##$name = SharePoint.wsp if($allowgacdeployment -eq $true) { stsadm -o deploysolution -name $name -url $url -force -immediate -allowgacdeployment } else { stsadm -o deploysolution -name $name -url $url -force -immediate } } function Retract-SPSolution() { param($name=$(throw "You must pass the name of the solution to retract"),$url="",[switch]$allcontenturls) ##$name = SharePoint.wsp if($allcontenturls -eq $true) { stsadm -o retractsolution -name $name -allcontenturls -immediate } else { if($url -eq "") { throw "You must pass the url if -allcontenturls is set to false" } else { stsadm -o retractsolution -name $name -url $url -immediate } } } function Delete-SPSolution() { param($name=$(throw "You must pass the name of the solution to delete")) ##$name = SharePoint.wsp stsadm -o deletesolution -name $name } function Upgrade-SPSolution() { $filename = “HARD CODE PATH TO VISUAL STUDIO WSP OUTPUT FILE” $name = “HARD CODE SOLUTION NAME” $url = “HARD CODE URL” Retract-SPSolution –name $name –url $url Delete-SPSolution –name $name Start-Sleep –seconds 5 Add-SPSolution –filename $filename Deploy-SPSolution –name $name -url $url }
Again, there is some flexibility here in where your configuration files are deployed and when they are moved to their final resting place. I like to package everything up into one WSP, so my config files will be deployed out to the CONFIG folder in the 12 Hive. This also makes it very easy for me (or PowerShell) to go looking for them.
function Deploy-SPConfigFiles() {
trap [Exception] { continue;}
$12Hive = “C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\” $configSource = $12Hive + “CONFIG\<CompanyName>” $configDest = “D:\wss\virtualdirectories\<your web app folder>\” COPY $configSource $configDest -RECURSE -FORCE }
Lastly, and completely optional…some customer have configured their deployments to use TFS, NAnt, etc. and want to monitor progress during automated build and deploys. I don’t have any example scripts here, but remember PowerShell operates in a managed runtime. This means you can leverage the .NET assemblies in System.Data.SqlClient and System.Diagnostics to do some logging and error tracking. --------------------------
Now, to truly call this automation, we will rely on trusty old MSBuild. To add these PowerShell function to your projects is fairly straightforward: 1. Right click on the project, and choose Unload Project 2. Right click on the now “Unavailable” project, and choose Edit <Project Name> 3. Scroll to the bottom and add your property command to the <PropertyGroup> node <PropertyGroup> <CopyUserControls>powershell.exe import-module –name SPoShMod –force;Copy-SPControls –prjSrc $(ProjectDir)\UserControls –prjDest $(SolutionDir)Microsoft.SharePoint.UserControls</CopyUserControls> </PropertyGroup>
Then, just up a little bit above that, add the following section just after the <Import Project=”…….” /> node(s): <Target Name=”BeforeBuild”> </Target> <Target Name=”AfterBuild”> <Exec Command=”$(CopyUserControls)” /> </Target> 4. Now, close the project file you are editing. 5. Lastly, right click on your “Unavailable” project again and choose Reload Project Now, every time you build your web application, it will execute the PowerShell script and copy your user controls over to the feature project. Just apply this same technique for the other scripts and you’re good to go.
These scripts are all included in SPoshMod (THE SharePoint PowerShell Module) on codeplex. http://www.codeplex.com/SPoshMod
Developing in SharePoint is much like preparing a large holiday meal.  It involves strict ingredients