This one’s for you @SteveHolstad!
I was helping one of our teams ease the deployment of a Silverlight project into various server environments. The project contained a ServiceReferences.ClientConfig file to reference some WCF services in the solution. When deploying the project to a Development, Staging, or Production environment, the team had to:
- Un-XAP the Silverlight XAP
- Copy in an environment specific ServiceReferences.ClientConfig file (or edit)
- Re-XAP the Silverlight XAP
Here’s how to do this pretty easily with TFS Team Build.
I like to store environment specific settings in the source control branch that corresponds to that environment. In the Dev branch (which translates to our Development Integration environment), I created a version of ServiceReferences.ClientConfig with the appropriate environment specific service bindings and placed it at. $\MyTeamProject\Dev\Env\Config\Services\ServiceReferences.ClientConfig.
In the Team Project’s build definition, you can now tap into the BeforeCompile target and do the following:
<Target Name="BeforeCompile">
<CreateItem Include="$(SolutionRoot)\Env\Config\Services\ServiceReferences.ClientConfig">
<Output ItemName="ServiceReferences" TaskParameter="Include"/>
</CreateItem>
<Copy SourceFiles="@(ServiceReferences)"
DestinationFolder="$(SolutionRoot)\Source\MySilverlightProject"
OverwriteReadOnlyFiles="True"/>
</Target>
The first thing that a Team Build does is get the latest code from source control, and place it in a working directory on the build agent. The new BeforeCompile target replaces the ServiceReferences.ClientConfig in the working directory with the environment specific one.
And it does it BEFORE COMPILE! When TFS then compiles the MySilverlightProject project, the resulting XAP will contain the environment specific version of ServiceReferences.ClientConfig.
This of course assumes that you’re structured your source control in such a way where you can point different Team Build definitions at different branches or directories in your source tree.
This probably falls under SharePoint Administration 101, but as a developer, I was pleasantly surprised at how easy it was to move our MOSS 2007 intranet to a new web front end.
Our environment is very simple: 1 SharePoint WFE and a separate SQL Server which is shared by several other applications. Our old WFE recently started acting up and we figured that we better get a new server up and running before this one GPF’d. It also ran a 32-bit version of Windows Server 2003, and we wanted to upgrade to Windows Server 2008 x64.
It was also important for us to not have to do any DNS changes. If that doesn’t work for you, you’ll have to adjust these instructions slightly.
The whole process was pretty simple, here are the steps I followed:
- Install Windows 2008 Server x64 on the new server
- Install MOSS but do not run the SharePoint Products and Technologies Configuration Wizard
- Bring up the new server to the same patch level as the farm
- In this case, I took the opportunity to bring everything up to SP2
- Run the SharePoint Products and Technologies Configuration Wizard on the new server
- Connect the server to the farm
- Host the Central Administration site on the server
- Let the wizard do its thing
At this point, you have a another fully functioning WFE in your farm. However, it won’t be able to serve any requests because as far as DNS is concerned, it doesn’t exist. You can browse to your site at the server’s IP address to make sure that everything is working.
- Send a nice email to your users to let them know that the farm will be down while you do this
- Run the SharePoint Products and Technologies Configuration Wizard on the old server
- Disconnect the server from the farm
- Change the old server’s IP address to something else
Your farm is now inaccessible until you assign the old server’s IP address to the new server.
- Assign the old server’s IP address to the new server
- Install your SSL certificate (if applicable) on the new server
- Set your host headers in IIS on the new server
That’s it! Go through Central Administration and make sure all your services are started, you should be good to go.
A couple of interesting things I learned while doing this:
- Don’t know why you’d want to do this, but it looks like your WFEs can be running different version of Windows Server
- You have to apply your host headers manually in IIS. Not surprising because the wizard probably has no clue what you have going on for DNS and load balancing.
Now I need to work on getting Windows Rights Management Services off the old server!
SharePoint Saturday Chicago was this past Saturday, June 13th at the Tribune Tower in Chicago.
Kudos to Kris Wagner and the rest of the organizing committee for pulling off a great event, I’m already looking forward to next year.
My session was titled: Building Public Facing SharePoint Sites. Using a SharePoint publishing site that I built out for the presentation, I went over the following:
- Publishing workflow
- Topology of the solution
- Tips for organizing your Visual Studio solutions
- Using publishing site definitions
- Packaging your run-once assets
- Configuring and running content deployment
- Setting up anonymous access
- Special considerations for public sites
As promised, here is my presentation, and the complete source code for the publishing site that I demo’d.
There was some great discussion, it was helpful to hear about the experiences of other people working on SharePoint publishing sites – it was comforting to see that we all also suffered over the same issues!
I recently took on the role of INETA User Group Mentor for Illinois, Wisconsin, and Indiana. As a user group mentor, my responsibilities include being the groups’ primary INETA contact, as well as being among the first to hear about exciting programs coming out of INETA.
INETA recently announced the Regional Speakers Program to help formalize the process around identifying local speakers.
Some excerpts from the announcement:
This new initiative is designed to help user groups coordinate speaking engagements for meetings and community events such as code camps connect with local and regional speakers. Long term, we (INETA) hope to use this as a staging ground to evaluate regional speakers for the national Speakers Bureau. The first phase of the program, registering speakers and connecting user groups with Regional Speakers, is now available.
We invite you to register as a Regional Speaker on the INETA web site and enroll in the program. The registration is open to all, so please feel free to forward.
User Group leaders are encouraged to search for regional speakers and contact them directly via the website. The link will launch a peer-to-peer email conversation for you to make appropriate arrangements. The program is designed such that a user group could potentially schedule all of its speakers.
If you’re looking for the definitive reference for programming against Microsoft Office Communications Server 2007 R2, be sure to check out Programming for Unified Communications with Microsoft® Office Communications Server 2007 R2, published by Microsoft Press and now available at your favorite retailer of books!
Go get it at Amazon.com.
I helped one of the authors – Chris Mayo – review his chapters on programming against the Office Communicator SDK, so I’ve seen first hand the quality of the content in the book.
Chris will soon be publishing a code library that encapsulates the Office Communicator SDK and extends it to enable you to build Contextual Collaboration functionality into your UC-enabled applications. I’ll be sure to write about it when it becomes available.
I'll be speaking at SharePoint Saturday on June 13th at the Tribune Tower in Chicago.
For more more information, check out http://www.sharepointsaturday.org/chicago or follow @SPSaturdayChi on twitter. The event is free and is open to the public. Registration hasn't been opened yet, but keep an eye on the site for more information.
I'm really looking forward to my talk, it's titled Best Practices for Building Public Facing SharePoint WCM Sites.
Using a completely custom built SharePoint WCM site, I'm going to cover topics such as:
- Topology of a public facing SharePoint WCM site
- Solution architecture
- Organizing your Visual Studio and SharePoint solutions
- Using a custom publishing site definition
- Branding
- Considerations for public facing sites
- Content authoring
- Content deployment
- Authentication
I'll also make my slides and source code available immediately after my talk. Looking forward to seeing everyone there!
When creating a build definition in TFS, you can set the option to build each check-in, meaning that a team build will get queued up every time a developer checks in a changeset.
When you have multiple developers working on a project, your build drop location might get cluttered. It's even worse if you have different build definitions for various branches of your source tree, e.g. Dev, Main, and Prod.
My colleague @pwalke suggested that we create a CurrentBuild folder that would always contain the most current build for every build definitions. If you browse to the team project's build drop location, you can open the folder and grab the build output.
This was pretty easy to do in the build definition by overriding the AfterDropBuild build target:
<Target Name="AfterDropBuild" Condition="'$(BuildBreak)'!='true'">
<CreateItem Include="$(DropLocation)\$(BuildNumber)\**\*.*">
<Output ItemName="BuildOutput" TaskParameter="Include"/>
</CreateItem>
<RemoveDir Directories="$(DropLocation)\CurrentBuild\$(BuildDefinition)" />
<Copy
SourceFiles="@(BuildOutput)"
DestinationFolder="$(DropLocation)\CurrentBuild\$(BuildDefinition)\%(RecursiveDir)"
SkipUnchangedFiles="false"/>
</Target>
This clears out the directory, and then recursively copies the build output to a subfolder in the CurrentBuild folder.
Update: Added a condition on the build target to verify if the build was successful, thanks @sajo!
I'm a big fan of packaging "run once assets" into the deployment process of a SharePoint WCM site - I believe that this type of content should be deployed using Features until content authors ultimately assume responsibility for maintaining it.
The reason I insist on this is that I think it is critical for developers to always be working with the whole site, and not in a sandbox. Get developers seeing the big picture early on in a SharePoint WCM project, and you're much more likely to avoid integration issues down the line.
So when it came time to provision a hierarchy of sites as part of a custom site definition, I explored different ways to automate this.
One way of implementing custom provisioning logic is by including an implementation of SPWebProvisioningProvider in your site definition. You can tap into SPWebProvisioningProvider's InitializePortal method and execute custom logic, e.g., creating webs, or setting custom navigation properties.
When exploring Andrew Connell's Minimal Publishing Portal site definition (download the code for AC's Professional SharePoint 2007 WCM Development book), I noticed the ProvisionData attribute of a template in the site definition that pointed to a file called PortalConfig.xml. This looked promising, it seemed like a way to define your custom provisioning logic in markup instead of using the SharePoint object model.
I couldn't find any good examples of anybody using this in a site definition, so I fired up Reflector to see how SharePoint was using it when provisioning sites based on the Publishing Portal site definition.
Take a minute to check out the Publishing Portal template - you can find this in C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\1033\XML\webtempsps.xml:
<Template Name="BLANKINTERNETCONTAINER" ID="52">
<Configuration ID="0"
Title="Publishing Portal"
Hidden="FALSE"
ImageUrl="/_layouts/1033/images/IPPT.gif"
Description="... omitted ..."
ProvisionAssembly="Microsoft.SharePoint.Publishing, Version=12.0.0.0,
Culture=neutral, PublicKeyToken=71e9bce111e9429c"
ProvisionClass="Microsoft.SharePoint.Publishing.PortalProvisioningProvider"
ProvisionData="xml\\InternetBlank.xml"
RootWebOnly="TRUE"
DisplayCategory="Publishing"
VisibilityFeatureDependency="97A2485F-EF4B-401f-9167-FA4FE177C6F6">
</Configuration>
</Template>
The template defines a ProvisionClass of Microsoft.SharePoint.Publishing.PortalProvisioningProvider (which is simply an implementation of SPWebProvisioningProvider) and points to xml\\InternetBlank.xml in its ProvisionData attribute.
If you examine the Provision method Microsoft.SharePoint.Publishing.PortalProvisioningProvider in Reflector, you can follow the logic that SharePoint uses to provision a Publishing site.
The Provision method calls CreatePortal, which loads and validates InternetBlank.xml against the PortalTemplate.xsd schema. CreatePortal calls CreateChildWebs which recursively creates the site hierarchy as defined in InternetBlank.xml.

InternetBlank.xml can be found at C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\XML\InternetBlank.xml, let's take a look at its contents:
<?xml version="1.0" encoding="utf-8" ?>
<portal xmlns="PortalTemplate.xsd">
<web name="Home"
siteDefinition="BLANKINTERNET#0"
displayName="$Resources:cmscore,IPPT_Portal_Root_DisplayName;"
description="$Resources:cmscore,IPPT_Portal_Root_Description;" >
<webs>
<web name="PressReleases"
siteDefinition="BLANKINTERNET#1"
displayName="$Resources:cmscore,IPPT_Portal_PressRelease_DisplayName;"
description="" />
<web name="Search"
siteDefinition="SRCHCENTERLITE#1"
displayName="$Resources:cmscore,IPPT_Portal_SearchCenterLite_DisplayName;"
description="" />
</webs>
</web>
</portal>
If you've created an out of the box Publishing Portal, this should be familiar - InternetBlank.xml defines the Press Releases and Search Center sites that are created.
There are a couple of possible approaches you can take if you'd like to integrate this functionality into your custom site definition:
- Have your site definition use Microsoft.SharePoint.Publishing.PortalProvisioningProvider instead of writing your own SPWebProvisioningProvider
- Implement some of the logic in Microsoft.SharePoint.Publishing.PortalProvisioningProvider into your own implementation of SPWebProvisioningProvider
The approach you take really depends on your requirements. If you simply need to provision a hierarchy of sites as part of your site definition, go with #1. If you need to create those sites, but also execute more custom logic, go with #2.
I'm planning to explore a third option which involves extending Microsoft.SharePoint.Publishing.PortalProvisioningProvider to account for some more custom logic - we'll leave that for a future post though :)
Let me now demonstrate modifying AC's Minimal Publishing Portal site definition to use Microsoft.SharePoint.Publishing.PortalProvisioningProvider instead of a custom SPWebProvisioningProvider - you can download the code for AC's Professional SharePoint 2007 WCM Development book, the Minimal Publishing Portal site definition is in Chapter 5.
Let's first take a look at the WEBTEMP.PublishingMinimal.xml file for the site definition:
<?xml version="1.0" encoding="utf-8" ?>
<Templates xmlns:ows="Microsoft SharePoint">
<Template Name="PublishingMinimal" ID="10001">
<Configuration ID="0"
Title="Minimal Publishing Site"
DisplayCategory="Publishing"
Hidden="FALSE"
ImageUrl="/_layouts/images/PublishingMinimal/Preview.png"
RootWebOnly="false"
SubWebOnly="true" />
<Configuration ID="1"
Title="Minimal Publishing Portal"
DisplayCategory="Publishing"
Hidden="FALSE"
ImageUrl="/_layouts/images/PublishingMinimal/Preview.png"
ProvisionAssembly="Chapter05MinimalSiteDefinition, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=c591e70cfdf9ce4f"
ProvisionClass="WROX.ProMossWcm.Chapter05.ProvisioningEngine"
ProvisionData="SiteTemplates\\PublishingMinimal\\XML\\PortalConfig.xml"
RootWebOnly="true"
SubWebOnly="false" />
<Configuration ID="2"
Title="Minimal Publishing Site with Workflow"
DisplayCategory="Publishing"
Hidden="FALSE"
ImageUrl="/_layouts/1033/images/PublishingSite.gif"
SubWebOnly="true"
VisibilityFeatureDependency="54A92CA1-4E7C-4B73-B03A-E93955E4E560"
Description="... omitted ... "/>
</Template>
</Templates>
The site definition includes three configurations:
- Minimal Publishing Portal - the "provisioner" for the site definition
- Minimal Publishing Site - a template for a web without workflow
- Minimal Publishing Site with Workflow - a template for a web with workflow
Let's start by pasting the contents of C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\XML\InternetBlank.xml into PortalConfig.xml and editing the siteDefinition property of the root web to provision the appropriate configuration of the PublishingMinimal site definition:
<?xml version="1.0" encoding="utf-8" ?>
<portal xmlns="PortalTemplate.xsd">
<web name="Home"
siteDefinition="PublishingMinimal#0"
displayName="Home"
description="">
<webs>
<web name="PressReleases"
siteDefinition="BLANKINTERNET#1"
displayName="Press Releases"
description="" />
<web name="Search"
siteDefinition="SRCHCENTERLITE#1"
displayName="Search"
description="" />
</webs>
</web>
</portal>
We'll also modify WEBTEMP.PublishingMinimal.xml to use Microsoft.SharePoint.Publishing.PortalProvisioningProvider instead:
<?xml version="1.0" encoding="utf-8" ?>
<Templates xmlns:ows="Microsoft SharePoint">
<Template Name="PublishingMinimal" ID="10001">
<Configuration ID="0"
Title="Minimal Publishing Site"
DisplayCategory="Publishing"
Hidden="FALSE"
ImageUrl="/_layouts/images/PublishingMinimal/Preview.png"
RootWebOnly="false"
SubWebOnly="true" />
<Configuration ID="1"
Title="Minimal Publishing Portal"
DisplayCategory="Publishing"
Hidden="FALSE"
ImageUrl="/_layouts/images/PublishingMinimal/Preview.png"
ProvisionAssembly="Microsoft.SharePoint.Publishing, Version=12.0.0.0,
Culture=neutral, PublicKeyToken=71e9bce111e9429c"
ProvisionClass="Microsoft.SharePoint.Publishing.PortalProvisioningProvider"
ProvisionData="SiteTemplates\\PublishingMinimal\\XML\\PortalConfig.xml"
RootWebOnly="true"
SubWebOnly="false" />
<Configuration ID="2"
Title="Minimal Publishing Site with Workflow"
DisplayCategory="Publishing"
Hidden="FALSE"
ImageUrl="/_layouts/1033/images/PublishingSite.gif"
SubWebOnly="true"
VisibilityFeatureDependency="54A92CA1-4E7C-4B73-B03A-E93955E4E560"
Description="... omitted ... "/>
</Template>
</Templates>
While doing this, I discovered that I had to ensure that PublishingMinimal#0 and PublishingMinimal#2 also activated the SharePoint PublishingSite and PublishingWeb features. For each configuration in the site definition's ONET.xml, ensure that the features are activated.
The PublishingSite feature is scoped at the Site level and should be activated in <SiteFeatures>:
<SiteFeatures>
<!-- Feature: PublishingSite -->
<Feature ID="f6924d36-2fa8-4f0b-b16d-06b7250180fa" />
The PublishingWeb feature is scoped at the Web level and should be activated in <WebFeatures>:
<WebFeatures>
<!-- Feature: PublishingWeb -->
<Feature ID="94c94ca6-b32f-4da9-a9e3-1f3d343d7ecb" />
That's it!
Go ahead and create a site collection based on the Minimal Publishing Portal site definition, and you'll see the Press Releases and Search sites created automatically.
Creating custom site definitions is painful enough ... Simplifying it slightly by leveraging existing SharePoint Publishing functionality to provision a portal's hierarchy will hopefully make this a little easier.
As I mentioned, I'll be exploring extending the functionality in Microsoft.SharePoint.Publishing.PortalProvisioningProvider in order to still use it, but also inject some custom functionality which is often required during provisioning - stay tuned.
We recently wrapped up a project where we migrated a .com site built using Microsoft Content Management Server (MCMS) 2002 to take advantage of the web content management features available in SharePoint 2007.
Microsoft is no longer offering MCMS as a standalone product; all enterprise and web content management is now part of the MOSS platform. I'm not a Microsoft licensing expert (is anyone?), but customers with active Software Assurance for MCMS as of November 2006 are eligible for heavily discounted (or free) licenses for SharePoint 2007. At $50K a pop per server for a SharePoint Internet license, there's money to be saved here! Check out http://www.microsoft.com/cmserver/roadmap.mspx for some more information.
Anyway ... we really learned a lot of good stuff on this project, but also suffered through some other things the hard way. Together with the help of the rest of the team, I put together a list of the top ten things we learned on this SharePoint WCM project:
10 - CMS Assessment Tool - inventory your migration
Microsoft provides a tool for MCMS 2002 sites to "determine the level of work needed to migrate to SharePoint 2007". In our experience, this tool was really only good for one thing: getting an inventory of the assets in the MCMS 2002 site, e.g. the number of page templates, etc.
I wouldn't recommend using any output of this tool as input into the development process, the source code for your MCMS templates (if you have access to it) is more valuable than anything this tool puts out.
One thing I highly recommend is to store your old site's content in Xml files using the technique I described in my blog post Bulk Importing Content into a SharePoint Publishing Site to import it into the new site.
9 - Continuous Integration - how far to take it
The benefits of true continuous integration on a SharePoint project are debatable. By no means am I saying that continuous integration isn't valuable, it's just that the effort involved in actually making it work in SharePoint might just not be worth it.
We found a great middle ground: we have TFS configured to automatically build our source on every check in. After every successful build, all of our compiled WSPs are neatly arranged in our build drop location.
If you broke the build, you got yelled at over the next cube row ;)
8 - SSL - don't wait until the site is live
SSL is one of those things that developers tend to leave to the infrastructure and network dudes.
On this project, we implemented an HTTP module that would switch the site in and out of SSL depending on the area of the site that the user was visiting. We tried to fake it out in dev, but only saw the real issues once we had gone live with the site.
As cumbersome as this seems, get a real SSL certificate and make it available in an environment where developers can test any of the site's functionality related to SSL.
VeriSign even offers free trial SSL certificates (their sales reps won't stop calling you though) that developers can install in their individual environments.
7 - FireBug and Fiddler - the two amigos
Two development tools that we couldn't have gone without are FireBug and Fiddler.
The most visible part of the site migration process was to replicate the site's look and feel within SharePoint; this wasn't as straightforward as we initially thought. Forget the IE Developer Toolbar, FireBug is where it's at. It was invaluable when we were debugging CSS issues, or looking for the proper SharePoint style to override.
Oh yeah, and Heather Solomon rocks. Anyone that suffered through documenting every single SharePoint CSS style deserves some props.
Ah Fiddler, how I love thee. When we were knee deep in SSL issues, it was Fiddler that showed us the HTTP 301s that were causing FireFox and IE8 to complain about mixed content on the page.
6 - IETester - test your site on crap browsers (ahem, IE6)
Unfortunately, there are still quite a few IE6 installs out there, I wish it would just die already.
However, that meant that we couldn't ignore IE6 in our testing. IETester allowed us to easily test our site in different versions of IE without having to actually install any of those versions anywhere.
I don't run IE8 anywhere either, I'm really gun shy when it comes to installing beta versions of IE. IETester helped us spot any issues that came up when using different versions of IE, such as warnings about mixed content on a page when we were troubleshooting SSL issues. It's also worth nothing that IETester can also be configured to proxy through Fiddler for HTTP debugging.
5 - SharePoint Content Deployment - works great but tough to troubleshoot when things go wrong
Microsoft's recommended topology for an internet publishing web site is one where authors create content in a dedicated Authoring environment, and scheduled Content Deployment Jobs copy approved and published content to a Staging (optional) and Production (live) environment.
For the most part, this worked out pretty well for us. Here are a few things to watch out for:
Your content deployment destination site collection is NOT a Publishing site
When first setting up content deployment, you always deploy to a site collection that was created using the Blank Site site definition. No big deal, except that you need to understand that this site isn't truly a Publishing site. This makes sense in some ways - you don't want content authors to edit content on the live site, but it's confusing in some other ways.
For example, there are several options you would usually see under the site settings of a Publishing site, e.g. Caching, and Site Usage Reports. You won't see these options in your content deployment destination site, although this is where you would actually need to configure caching!!
The workaround here is that you have to know the urls to those options, they will work if you type them manually into the browser.
Troubleshooting failed content deployment jobs
Content deployment jobs fail for the damndest reasons, and when they do fail they're nearly impossible to troubleshoot. Maxime Bombardier has a great blog post called Debugging Content Deployment Issues, worth a look.
One technique for debugging failed jobs is to examine the exported package, and look in the manifest.xml for the item that failed. This will be the item which has a +1 count from the Number of Objects Imported. The problem with this technique is that it might sometimes point to an item that doesn't necessarily make sense, e.g. /Pages/Forms/AllItems.aspx - what on earth are you supposed to do here?
So if all else fails, what do you do?
One thing that worked consistently for us is the following:
- Switch the failed job to deploy ALL content (if it was initially set to incrementally deploy content)
- Run the full content deployment job
- This might take a while, make sure there's enough space for the SQL transaction logs to grow
- Verify that the full content deployment job succeeded
- Reset the job back to incremental
- Run all the other jobs you have configured along this content deployment path at least once (more on why next)
A side effect of running a FULL content deployment job is that it might reset any custom permissions you have implemented on sub sites in your site collection - even though you configure the job to not deploy any security settings.
Additionally, any incremental content deployment jobs that run after the full job might do the same.
We never figured out why exactly this happens, which is why we run all the incremental jobs again, and then correct any of the security settings that were messed up.
Yay for lame workarounds!
4 - Andrew Connell's WCM Book - worth its weight in gold
Professional SharePoint 2007 Web Content Management Development, really the best SharePoint book out there.
Don't let the title fool you, this is a must for every SharePoint developer - WCM or not.
I love the approach that Connell uses when demonstrating how to build a piece of functionality. He shows you how do it in the SharePoint UI, how to do it in SharePoint Designer, and how to do it using SharePoint Features and Solutions.
Just packed with awesomeness, and small enough that you can always carry it around.
While you're at it, check out @andrewconnell on twitter. Really easy to get in touch with, and always more than happy to answer questions.
3 - CSSRegistration - order matters
Typically, you will link to one or more CSS files from within your master page. You might have special taken care to order your CSSRegistration controls - after all, the order of CSS declarations matters.
Well, don't bother. SharePoint will render them alphabetically... !@#!#@$
2 - The F5 Experience - not
Simply put, there is no F5 experience in SharePoint. If you use tools such as the Visual Studio Extensions for WSS, you're going to make tradeoffs in some other places.
So your head doesn't explode a few weeks into the project, take the time up front to optimize your team's edit-compile-deploy process.
For example, use MSBuild targets to automatically build your WSP and generate your install/deploy/retract/remove/upgrade scripts. Instead of manually doing everything via Central Administration and Site Collection Features, compile and run one or more of your scripts, or have a script that does a retract/remove/install/deploy in one step!
Work smarter, not harder!
1 - Feature Receivers and HTTP Modules - the control you need
Feature Receivers
If you find yourself manually pasting content in your web.config file as part of your deployment process, step back and look at automating the process.
A few of our features depended on the existence of certain sections and items in a SharePoint site's web.config file. We wanted to have these sections automatically created and deleted as part of our feature activation and deactivation process.
For example, if a feature depended on the existence of a custom web.config section, we would store the text for the section in a resx file, and then create it using the SPWebConfigModification class as part of our feature activation process. Taking this a step further, you could use a different version of the resx file depending on which source control repository you were building out of.
There are a lot of uses for feature receivers, we just found this one particularly useful.
HTTP Modules
Before this project, I hadn't written a single HTTP module. I can't say that anymore, here are some examples of functionality we implemented using HTTP modules:
- Override Layouts pages master page
- Custom 500 error page
- Custom authentication for different sub sites in a site collection
- Switch site in and out of SSL depending on the site or page being requested
You might be intimated by the idea of writing an HTTP module, but they're actually no big deal since they're pretty easy to implement. Just remember though, that the order that you list them in your web.config actually matters.
Also keep in mind that each module will execute for every request, there will be performance implications if you go nuts and implement a lot of these.
Hopefully you find this list somewhat useful, and it saves you some head banging on your next SharePoint project. Would love to hear your suggestions in the comments, or catch me on twitter @gdurzi.
I recently wrote about a Combined SSL HttpModule for SharePoint Webs and Pages that we used on a project to switch a SharePoint site in and out of SSL for certain _layouts pages as well as specific webs within the site collection.
Turns out there was a flaw in the HttpModule that appeared when I was using newer builds of FireFox and IE8. With the HttpModule enabled, I would get browser warnings that my entire connection wasn't encrypted.
FireFox handled this somewhat gracefully, putting a red exclamation mark over the padlock icon. IE8, on the other hand prompted the user if they would like to load the page without the items in question, causing the page to barf <-- technical term.
After spending a lot of time analyzing the traffic with Fiddler (a must in every developer's toolkit), as well as a very helpful suggestion on StackOverflow, I figured out the cause of the issue.
When analyzing the traffic in Fiddler, you could see that the request started as SSL, but was followed by a 301 response, redirecting it to a non-HTTPS link to the resource I was retrieving.
This was happening for items I had in my master page (using relative links!), e.g. my CSS, JavaScript, and general images for look and feel. Interestingly, IE6, IE7, and Chrome ignored the error and showed my site as incorrectly running full SSL. Odd given that I now understood that some resources were in fact not being transmitted over SSL.
Obviously the HttpModule will fire even for requests for "non-HTML" content. Knowing this, I modified the HttpModule to ignore that type of content.
Check out the original post for most of the background code, here's the updated PreRequest handler:
HttpContext ctx = HttpContext.Current;
SPContext spContext = SPContext.Current;
if (spContext != null)
{
if (spContext.Web != null)
{
// Assume that request does not require SSL
bool requestRequiresSSL = false;
// Check if the current request is for page content
// not gif, js, css, etc.
bool isHTMLContent = !_ignoreExt.Contains(
Path.GetExtension(ctx.Request.PhysicalPath).ToLower());
// Get the request Path and QueryString value
string pathAndQuery = ctx.Request.Url.PathAndQuery;
if (isHTMLContent)
{
// Layouts pages need to be handled separately
//
if (pathAndQuery.ToLower().Contains("_layouts"))
{
// Check if the current layouts page requires SSL
foreach (var page in _sslPages)
{
if (pathAndQuery.ToLower().Contains(page.ToLower()))
{
requestRequiresSSL = true;
break;
}
}
}
else
{
// Check if the current site requires SSL
if (spContext.Web.Properties.ContainsKey
(SITE_REQUIRES_SSL_PROPERTY))
requestRequiresSSL = (spContext.Web.Properties
[SITE_REQUIRES_SSL_PROPERTY].ToString()).ToLower()
== Boolean.TrueString.ToLower();
else
return;
}
}
if (isHTMLContent)
{
// SSL required and current connection is not SSL
if (requestRequiresSSL && !ctx.Request.IsSecureConnection)
ctx.Response.Redirect(_baseURLWithSSL + pathAndQuery);
// SSL not required but current connection is SSL
if (!requestRequiresSSL && ctx.Request.IsSecureConnection)
ctx.Response.Redirect(_baseURLNoSSL + pathAndQuery);
}
We're wrapping up a project where we're building a publicly accessible dot com site in SharePoint.
When designing the topology for the site, we followed Microsoft's guidance for Publishing sites where no authoring happens directly on the live site. Instead, we have a dedicated Authoring environment which is part of the intranet SharePoint farm. Content Deployment jobs are then configured to deploy content (what else) to the live site, which lives in its own SharePoint farm out in the DMZ.
We recently enabled Kerberos in the farm and switched all web applications to use Negotiate authentication instead of NTLM. So everything including Central Administration, the Shared Services Provider, and all content web applications now authenticated users using Kerberos.
One morning, I noticed that I hadn't yet received the nightly "Content Deployment Job Succeeded" email. I went into Central Administration to check on the Content Deployment job and noticed that it was stuck in "Preparing".
A quick Google yielded a bunch of results about content deployment jobs being stuck in Preparing mode. However, they all pointed to an issue which was fixed in the Infrastructure Updates. We were fully and currently patched, so I figured something else was causing this.
In my research, I found a blog posting by Stefan Goßner called Pimp My Content Deployment Job. In the posting Stefan points to a hidden list which shows all the Content Deployment paths configured in the farm. I guess it's not really hidden, since you can find it via Site Settings --> Lists and Libraries.
Upon examining the specific Content Deployment path, I noticed that it's AuthenticationType property was set to NTLM
I edited the ListItem (highly unsupported), and saw that AuthenticationType was just a text property. Take a look at the documentation on MSDN, it's just a String property ... I had hoped it would be an enum or something more intuitive.
I tried changing the value of AuthenticationType to Kerberos, or Negotiate, resetting IIS, and firing off my Content Deployment job again without success. It would always get stuck in Preparing.
Unfortunately, the only thing that resolved it was to change the Authentication Provider for my web application back to using NTLM.
In my last post - Bulk Importing Content into a SharePoint Publishing Site - I described a technique that we used to programmatically import content into a SharePoint publishing site.
There are some great things about this approach, most importantly:
- Your content becomes part of your source control
- You don't have to create the site content by hand
- You can import the content as many times as you need to
The site we are working on relies heavily on SharePoint Reusable Content; content that is stored and authored in one location, but displayed in several. We decided to extend our bulk import process to accommodate Reusable Content.
Storing Reusable Content in Xml Files
Just as we did with normal publishing content, we store the site's Reusable Content in XML files. Since Reusable Content is simply just another list in a SharePoint publishing site, we can provide values for the various columns in the list, e.g. Title and Reusable HTML.
<?xml version="1.0" encoding="utf-8" ?>
<Items>
<ReusableContent>
<Columns>
<Column Name="Title">Sample Reusable Content</Column>
<Column Name="ID">1</Column>
<Column Name="ContentCategory">General</Column>
<Column Name="AutomaticUpdate">1</Column>
<Column Name="Reusable HTML">
<![CDATA[
<p>reusable content HTML</p>
]]>
</Column>
<Column Name="Comments">
<![CDATA[
]]>
</Column>
</Columns>
</ReusableContent>
</Items>
Note that one not-so-elegant step we had to take was to provide IDs to the List items ourselves. The only negative of this approach is that before importing Reusable Content again, we would have to clear out the existing items from the list.
Importing the Reusable Content
var query = from xElem in xmlFile.Descendants("ReusableContent") select new Reusable
{ Columns = (from column in xElem.Descendants("Column") select new Column
{ Name = column.Attribute("Name").Value, Value = column.Value
}).ToList()
};
SPSite siteCollection = null;
SPWeb site = null;
string siteURL = ConfigurationSettings.AppSettings[CONFIG_SITEURL];
using (siteCollection = new SPSite(siteURL))
{ using (site = siteCollection.RootWeb)
{
foreach (var reusable in query)
{
try
{ Console.WriteLine("Adding new reusable content to list..."); SPListItem listItem = null;
listItem = site.Lists["Reusable Content"].Items.Add();
Console.WriteLine("Processing reusable content columns...\n"); foreach (Column siteColumn in reusable.Columns)
listItem[siteColumn.Name] = siteColumn.Value;
Console.WriteLine("Completing item workflow..."); //update item to complete process
listItem.Update();
}
catch (Exception ex)
{ throw (ex);
}
}
}
Same idea here ... We query an XML file containing our Reusable Content using some simple LINQ. We then iterate through the result set and add the ListItems one by one to the Reusable Content list.
Referencing the Reusable Content in our Import Files
The rest of the site content still lives in our XML import files, and some of that content now needs to be updated to reference a specific item of Reusable Content.
Again, one thing to point out here is that we're sort of hardcoding the ID of the item. Good enough for our quick and dirty console application that we use to populate the content in our site.
Check out the previous post for more information on these import files, but we now directly reference the Reusable Content item in the columns that will use it. (Guids trimmed for readability)
<Page
Name="Sample.aspx"
Default="false"
ContentTypeId="some really long content type id ..."
PageLayout="SampleLayout.aspx">
<Columns>
<Column Name="Title" Id="">Some Title</Column>
<Column Name="ColumnUsingReusableContent" Id="">
<![CDATA[
<div id="__publishingReusableFragmentIdSection">
<a href="/ReusableContent/1_.000">a</a>
</div>
<span id="__publishingReusableFragment"></span>
]]>
</Column>
</Columns>
</Page>
Now go reuse some content!
A little while ago, I posted a question on StackOverflow looking for suggestions on how best to migrate content into a SharePoint publishing site. I got a quite a few good suggestions, but none of them felt like a great fit for our needs.
As you may have realized from my last few posts, I'm working on a project to migrate an existing web site to SharePoint. A very important thing for me was to have all the site content in source control, allowing us to be able to repeatably deploy it into SharePoint during development and testing.
Additionally, we were working with a topology where all authoring would be happening in an Authoring environment behind the firewall, with scheduled Content Deployment jobs responsible for migrating the content to the Production environment. We had to be able to initially populate the content into the Authoring environment so that the content authors (or us) didn't have to do it manually.
We came up with what I think is a pretty original approach to this. The general idea is that since this is a SharePoint Publishing site, all of our content obviously corresponds to a certain content type (inheriting from the Page content type), and is displayed using a specific Page Layout.
Content Type Import Template
What this enables us to do is to define our site content in Xml - making it part of our source code. We can use a simple application to deploy it to a specific site collection.
<?xml version="1.0" encoding="utf-8" ?>
<Pages SiteUrl="/">
<Page
Name="AboutUs.aspx"
Default = "false"
ContentTypeId=""
PageLayout="GeneralContent.aspx">
<Columns>
<Column Name="Title" Id="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}">About Us</Column> <Column Name="Page Content" Id="{f55c4d88-1f2e-4ad9-aaa8-819af4ee7ee8}"> <![CDATA[
<p>Some Html</p>
]]>
</Column>
<Column Name="Meta_Keyword" Id="{74dcba16-0809-4bca-87d5-b0fc67d2086f}"></Column> <Column Name="Meta_Description" Id="{6b1216bb-3023-462d-af6f-31878e9808bb}"></Column> </Columns>
</Page>
</Pages>
Page Element
What's happening here is actually pretty simple; our Xml file contains a Page element for every Publishing page that we need to import into the SPSite at the url specified by the SiteUrl property.
There are several attributes of the Page element:
- Name - the name of the page when it's created in the Pages document library
- Default - whether or not this is the Welcome Page of the specific site
- ContentTypeId - the id of the content type that describes this content (I omitted it from the snippet above because content type ids are so damn long)
- PageLayout - the page layout to use when creating this page
Columns
Each Page also contains a collection of columns corresponding to the columns in the content type. As I mentioned, all of our content types inherited from the Page content type, allowing us to use its columns such as Title and Page Content.
For each column, we provide the Name and the Id. We went with this approach for two reasons, the first being readability of our import files. The second being that when creating these pages programmatically, we felt that it was safer to reference these columns by their Id.
Dealing with HTML Content
The Page Content column contains Publishing HTML, we can specify this in our Xml import files by wrapping the HTML content in a <![CDATA[ ]]> block. This is great because we can paste our HTML in here without worrying about the Xml validation.
Importing the Content
Before we look at the code to import this content, let's define a couple of classes that will help us:
public class Page
{ public string Name { get; set; } public string ContentTypeId { get; set; } public string PageLayout { get; set; } public List<Column> Columns { get; set; } public bool Default { get; set; }}
public class Column
{ public string Name { get; set; } public Guid Id { get; set; } public string Value { get; set; }}
After specifying a particular Xml file to import content from, we use some LINQ to get the set of Pages:
var query = from xElem in xmlFile.Descendants("Page") select new Page
{ Name = xElem.Attribute("Name").Value, ContentTypeId = xElem.Attribute("ContentTypeId").Value, PageLayout = xElem.Attribute("PageLayout").Value, Default = Boolean.Parse(xElem.Attribute("Default").Value), Columns = (from column in xElem.Descendants("Column") select new Column
{ Name = column.Attribute("Name").Value, Id = new Guid(column.Attribute("Id").Value), Value = column.Value
}).ToList()
};
This now becomes a matter of iterating through each Page element and programmatically creating and publishing the Page:
SPSite siteCollection = null;
SPWeb site = null;
PublishingSite publishingSite = null;
PublishingWeb publishingWeb = null;
foreach (var page in query)
{
using (siteCollection = new SPSite(siteURL))
{ using (site = siteCollection.AllWebs[siteName])
{ publishingSite = new PublishingSite(siteCollection);
publishingWeb = PublishingWeb.GetPublishingWeb(site);
List<PublishingPage> pages =
publishingWeb.GetPublishingPages().ToList();
PublishingPage thePage = pages.Find(
delegate(PublishingPage pp)
{ return pp.Name == page.Name; });
if (thePage != null)
{ // Page exists
thePage.CheckOut();
}
//get ContentTypeID
SPContentTypeId contentTypeId =
new SPContentTypeId(page.ContentTypeId);
//get PageLayout and Page Name
List<PageLayout> layouts =
publishingWeb.GetAvailablePageLayouts(contentTypeId).ToList();
PageLayout layout = layouts.Find(
delegate(PageLayout pl)
{ return pl.Name == page.PageLayout; }); string pageName = page.Name;
PublishingPage pubPage = null;
if (thePage != null)
pubPage = thePage;
else
pubPage = publishingWeb.GetPublishingPages().Add(pageName, layout);
foreach (Column siteColumn in page.Columns)
pubPage.ListItem[siteColumn.Id] = siteColumn.Value;
pubPage.Update();
//publish and approve
SPListItem newPage = pubPage.ListItem;
newPage.File.CheckIn("Checked in on creation"); newPage.File.Publish("Published on creation"); }
}
}
Conclusion
There are a bunch of things we can improve on here, but we just needed a quick and dirty tool that provided us with a repeatable way of creating our site content. We can now fully populate our Authoring environment and let the Content Deployment job take care of the rest.
UPDATE (03/08/2009): See updated post meant to handle an issue where entire page content wasn't being transmitted over SSL.
I'm currently working on a .com SharePoint site where access to several sub-sites is restricted to logged in users. The team also implemented custom login and registration pages for these sites as layouts pages.
We have a requirement that these sub-sites and the layouts pages both use SSL.
I came across two very helpful blog posts that addressed each scenario individually:
With very little tweaking, each of these handles the particular situation it was designed for very well. However, I basically needed a combination of these two approaches.
If I tried enabling both HttpModules on my SharePoint web application, one scenario would work, but not the other. So I decided to combine both approaches into a single HttpModule:
1: public class SSLHttpModule : IHttpModule
2: { 3: private HttpApplication _application;
4:
5: private String _baseURLNoSSL;
6: private String _baseURLWithSSL;
7: private String _sslPagesList;
8:
9: private List<String> _sslPages = new List<string>();
10:
11: const String BASEURL_NOSSL = "BaseUrlNoSSL";
12: const String BASEURL_SSL = "BaseUrlSSL";
13: const String SITE_REQUIRES_SSL_PROPERTY = "requiressl";
14: const String SSL_LAYOUTS_PAGES = "SSLLayoutsPages";
1:
2: public void Init(HttpApplication application)
3: { 4: if (ConfigurationManager.AppSettings[BASEURL_NOSSL] != null)
5: _baseURLNoSSL = ConfigurationManager.AppSettings[BASEURL_NOSSL];
6: else
7: return;
8: if (ConfigurationManager.AppSettings[BASEURL_SSL] != null)
9: _baseURLWithSSL = ConfigurationManager.AppSettings[BASEURL_SSL];
10: else
11: return;
12: if (ConfigurationManager.AppSettings[SSL_LAYOUTS_PAGES] != null)
13: _sslPagesList = ConfigurationManager.AppSettings[SSL_LAYOUTS_PAGES];
14: else
15: return;
16:
17: // Create a List<String> of Layouts pages requiring SSL
18: //
19: char[] separator; separator = new char[] { ',' }; 20: string[] pages; pages = _sslPagesList.Split(separator);
21: _sslPages.AddRange(pages);
22:
23: application.PreRequestHandlerExecute += new EventHandler(PreRequest);
24: _application = application;
25: }
There are three application settings (that live in web.config) that make the whole thing work:
- BaseUrlNoSSL - the url of your site when you're not using SSL
- BaseUrlSSL - the url of your site when you're using SSL
- I use an Alternate Access Mapping to do this
- SSLLayoutsPages - a comma-separated list of layouts pages which should be secured by SSL
- Nothing's stopping you from modifying this code to make it work with any page, not just layouts pages.
The following listing shows the PreRequest event of the HttpModule. What I'm doing differently here is handling the case for layouts pages first, and then checking if the SPWeb needs SSL.
1: public void PreRequest(object sender, EventArgs e)
2: { 3: HttpContext ctx = null;
4: SPContext spContext = null;
5:
6: try
7: { 8: ctx = HttpContext.Current;
9: spContext = SPContext.Current;
10: if (spContext != null)
11: { 12: if (spContext.Web != null)
13: { 14: string pathAndQuery = ctx.Request.Url.PathAndQuery;
15:
16: if (pathAndQuery.ToLower().Contains("_layouts")) 17: { 18: bool pageRequiresSSL = false;
19:
20: foreach (var page in _sslPages)
21: { 22: if (pathAndQuery.ToLower().Contains(page.ToLower()))
23: { 24: pageRequiresSSL = true;
25: break;
26: }
27: }
28:
29: if (pageRequiresSSL)
30: { 31: if (HttpContext.Current.Request.Url.Scheme.ToString() == "http")
32: { 33: string url = HttpContext.Current.Request.Url.ToString();
34: url = url.Replace("http://", "https://"); 35: HttpContext.Current.Response.Redirect(url);
36: }
37: }
38: else
39: { 40: if (HttpContext.Current.Request.Url.Scheme.ToString() == "https")
41: { 42: string url = HttpContext.Current.Request.Url.ToString();
43: url = url.Replace("https://", "http://"); 44: HttpContext.Current.Response.Redirect(url);
45: }
46: }
47: }
48: else
49: { 50: bool siteRequiresSSL = false;
51: if (spContext.Web.Properties.ContainsKey(SITE_REQUIRES_SSL_PROPERTY))
52: { 53: siteRequiresSSL = (spContext.Web.Properties
54: [SITE_REQUIRES_SSL_PROPERTY].ToString()).ToLower()
55: == Boolean.TrueString.ToLower();
56: }
57:
58: if (siteRequiresSSL & !ctx.Request.IsSecureConnection)
59: { 60: if (_baseURLWithSSL != null)
61: ctx.Response.Redirect(_baseURLWithSSL + pathAndQuery);
62: else
63: ctx.Response.Redirect(String.Concat("https://", 64: ctx.Request.Url.Host, pathAndQuery));
65: return;
66: }
67: if (!siteRequiresSSL & ctx.Request.IsSecureConnection)
68: { 69: if (_baseURLNoSSL != null)
70: ctx.Response.Redirect(_baseURLNoSSL + pathAndQuery);
71: else
72: ctx.Response.Redirect(String.Concat("http://", 73: ctx.Request.Url.Host, pathAndQuery));
74: return;
75: }
76: }
77: }
78: }
79:
80: }
81: catch (Exception) { } 82: }
Some notes about this:
- I provisioned the RequireSSL property of each SPWeb as part of our provisioning process in a custom site definition.
- We provisioned all the required web.config modifications in a feature receiver, definitely a much more elegant way than manually modifying your web.config file
I'm currently working on an anonymously-accessible internet SharePoint website with a splash page for the home page, i.e. it has a completely different look and feel than the rest of the site.
The way we implemented this was to build a separate master page and page layout for the home splash page, where the page layout would override the default master page in its code behind.
When a page layout has a code behind, its declarations are a little different than your typical page layout that inherits from PublishingPage
1: <%@ Assembly Name="PageLayouts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=" %>
2:
3: <%@ Page Language="C#" Inherits="PageLayouts.HomeSplash" %>
In the code behind of the file, we override the site's master page in the OnPreInit event:
1: public class HomeSplash : LayoutsPageBase
2: { 3: protected override void OnPreInit(EventArgs e)
4: { 5: this.MasterPageFile = "homesplash.master";
6: }
7: }
However, when extended the site into the Internet zone and enabled anonymous authentication (via forms), pages created based on the HomeSplash page layout where triggering the site to ask the user to login.
Turns out that LayoutsBasePage isn't the right type to use if you want this page to be anonymously accessible, instead the page layout needs to inherit from UnsecuredLayoutsBasePage.
1: public class HomeSplash : UnsecuredLayoutsPageBase
2: { 3: protected override bool AllowAnonymousAccess { get { return true; } } 4:
5: protected override void OnPreInit(EventArgs e)
6: { 7: this.MasterPageFile = "homesplash.master";
8: }
9: }
We also override a property of UnsecuredLayoutsBasePage called AllowAnonymousAccess to denote that pages based on this page layout should be anonymously accessible.
More Posts
Next page »