Friday, October 14, 2011

SharePoint Designer And Developer Collaboration: Integrating Site Columns, Content Types, and SPD Page Layouts Into Visual Studio For Proper Deployment

Chris Domino, Director, Enterprise Architect


My current project, due to an extremely tight budget and resource constraints, started out with only half the team: our SharePoint Designers, configurators, and administrators. Their task was to do the information architecture and get the site up as minimally as possible so that the client's content authors can start, well, content authoring.

So first populate the site, and then bring in the developers to build it. Interesting approach. What's more interesting, is that with some careful architectural planning, it's also a very lucrative approach. It allows a large team of people will developments skills that transcend all the layers of SharePoint to crank out a project with an insane timeline without stepping on each other's toes in any way.

In this post, I'd like to discuss this collaboration with two different asset examples: site columns and content types, and page layouts and master pages. The idea is to let your SharePoint Designers loose on a live, vanilla site, so they can quickly build out the information architecture directly in production, and write the page layouts and master page in SharePoint Designer. For those of you who read me often, no, I did not just get diagnosed with multiple personality disorder and started allowing my worst-practice alter ego to post to my blog.

Doing any development whatsoever in production is bad.

Manually configuring your SharePoint environment without the use of a script or features is bad.

And I'll say it: using SharePoint Designer to hack away at a site that will have a lot of custom, code-built functionality is bad.

When I say "bad" above, I don't mean "bad" as in "Don't do it." I say bad because it allows the potential for non-technical users to do things in a static, unrepeatable manner. My team is fortunately full of experienced SharePoint developers who know their tools, and more importantly, their limits. When the non-programmer members of our team created the requirements document and delegated which features were "native creative" and which where custom code, they nailed it; I didn't have to make a single change.

So that allowed them to create the site very quickly (which is the one big benefit to the aforementioned "bad" practices) while we built out our solution infrastructure: TFS, Visual Studio 2010 / SharePoint 2010 project integration, baseline architecture, and, most importantly, deployment package skeleton. Our plan was to achieve these two tasks concurrently as a first step. Step two was then to suck all their assets into Visual Studio, nestle them into the SharePoint 2010 project structures, and write a little code in preparation for a WSP deployment proper.

Site Columns And Content Types

This one is the easier of the two. In order to quickly Visual Studio-inze the site columns and content types, we can use the SharePoint integration into the Server Explorer to connect to the site and import the assets. The only issue with this is that it must be a local site. So I had the client give me a site collection backup, and used PowerShell to restore it to a new one on my box.

Once this was stood up, I was able to connect to it from Visual Studio (with CKS DEV extensions installed). Here's how:

  1. From the "View" menu, select "Server Explorer."

  2. Click the "Add SharePoint Connection" button.

  3. Type in the URL to a local site collection.

  4. Make sure you have a SharePoint project highlighted in Solution Explorer (that doesn't already have any SharePoint assets imported). Under the new node that's created in Server Explorer, navigate to the "Site Columns" sub node, and then select the Site Column group (rendered as a folder) or a single site column itself. Right click it, and select "Import Site Columns." Visual Studio will add standard SharePoint site column assets with Elements.xml files, all ready to be wrapped into WSP deployment and feature activation.

  5. Repeat Step #'s 4 and 5 for content types.

A feature will be created for you if there isn't already one. You can add your own features or import these assets into different SharePoint projects to organize these elements and their deployments however you want. I like to separate the solutions because with all these files things can get cluttered, and putting them into folders (and carefully making sure all relative URL references in the Element.xml files are correct) doesn't seem to work.

One extra task is to make sure things will be deployed in the correct order. Since content types depend on site columns, site columns need to be activated first. Also, if you have content type inheritance, base content types need to be deployed before any of their children (otherwise you'll get an error in Visual Studio upon a CKS "deploy with activation" operation saying that the parent content type doesn't exist). You might have to manually edit the Feature.xml file for your feature, and manipulate the XML so that the order is correct.

Here's how to do that:

  1. Expand the feature, and double click the "Feature1.feature" node in Solution Explorer.
  2. In the Feature Designer, click "Manifest."
  3. Plus out "Edit Options."
  4. Click "Overwrite generated XML and edit manifest in the XML Editor."
  5. Click "Yes" on the overly-cautious pop up.

Now we can get at the XML:

  1. Click "Edit manifest in the XML editor." This actually disables the designer for this feature.
  2. Update your ElementManifest nodes so that normal site columns are first, then lookup site columns, then base content types, and finally derived content types. Save the finally normally. As you could tell in the above screen shot, you can revert back to the generated order by repeating the proceeding procedure and clicking "Discard manifest changes and re-enable the designer" and then "Yes" on the pop up window.

And that's it; Visual Studio now "owns" the site columns and content types. It's best if you can blow away the site collection before deploying from a Visual Studio-packaged WSP, or at least delete all the assets you're about to push. If you can't, the deployment still works; updates are performed instead of inserts. I have seen columns get duplicated in inherited content types down in lists and libraries, and other weirdness when we had to deploy over existing content. In this project, we fortunately had the luxury of blasting all existing content, so I didn't have to explore the update scenario further; I would be shocked if you couldn't get it to work somehow.

Page Layouts and Master Page

The other integration point is for our pages. This one is much more interesting; not only do we get to write some code, but it also solves the greater problem of page creation workflow between SharePoint designers and developers. Let's delve into that first, and look at the feature receiver that does the work second. This paradigm is really the crux of this post.

Like I said, making changes in production is bad. In addition to the issues I pointed out before, it makes tasks like the ramping up of new project resources or provisioning development and staging environments very difficult and time consuming. Having a dev environment that doesn't match prod is a really easy way to introducure regression bugs into your build. Even if you've gone through a checklist of all the manual tweaks needed to stand the site up proper, it's easy to miss something. This is why we need Visual Studio-generated WSP's to "own" all facets of deployment.

That said, making changes in production is fast, and given the skillsets of certain members of the team and timeline or resource constraints of the project, it just might be the best way to go. This is especially true for the first iteration of a project; I have no problem treating production as a development server before the site goes live. You can simply get away with being sloppier. But once it's pushed, we need to exercise much deployment diligence if updates are to be applied seamlessly.

So here's the procedure for integrating page layouts (master pages follow the same rules, so I'll omit them except when they differ) created in SharePoint Designer into Visual Studio. With these, there's no issue with creates verses updates; even if an instance of a page based off one of these layouts exists, we can totally run this code to update the layout without breaking anything.

First things first: we need a page. Create a new ASPX page in Visual Studio (with code behind) and place it somewhere under your mapped SharePoint Layouts folder. It's fastest to create a new SharePoint Application Page and change the base class to PublishingLayoutPage, since ASP.NET templates aren't available in SharePoint projects. You might have to add a reference to Microsoft.SharePoint.Publishing.

Copy and paste the HTML from SharePoint Designer (or wherever) into the ASPX file.

At the top of the page, replace the @Page directive with the following two lines of markup (of course using your class names):

  1. <%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
  2. <%@ Page Language="C#" MasterPageFile="~masterurl/default.master" CodeBehind="DDDWebPartPageLayout.aspx.cs" Inherits="DDD.Web.DDDWebPartPageLayout" %>

In Line #1, we take advantage of the Visual Studio / SharePoint 2010 integration feature that tokenizes our FQNs. This is what allows us, in Line #2, to not have to fully-qualify the class name for the value of the "Inherits" attribute (like SharePoint otherwise normally requires).

Repeat this for all page layouts, and your master page. For the master page, you of course use an @Master directive instead of @Page, and omit the "MasterPageFile" attribute in the HTML. And in the code behind, you inherit from the plain old ASP.NET MasterPage class. However, since all these pages will either live in the master page gallery or within _layouts in 14, they can all follow the same procedure.

Now that our layouts are in Visual Studio, we need to prepare them for WSP deployment. First, create a module for each file. Here's what a sample Elements.xml would look like:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="">
  3. <Module Name="PageLayouts" SetupPath="Layouts\DDD.Web\pages" Url="_catalogs/masterpage" RootWebOnly="TRUE">
  4. <File Url="TestPageLayout.aspx" Path="TestPageLayout.aspx" Type="GhostableInLibrary" IgnoreIfAlreadyExists="TRUE">
  5. <Property Name="ContentType" Value="$Resources:cmscore,contenttype_pagelayout_name;">
  6. <Property Name="PublishingAssociatedContentType" Value=";#Test Page Layout Content Type;#0x010100C568DB52D9D0A14D9B2FDCD97266E9F2007948130EC3DB064584E219954237AF39050201" />
  7. <Property Name="Title" Value="Test Page Layout" />
  8. <Property Name="Description" Value=" This is a test Page layout." />
  9. </File>
  10. </Module>
  11. </Elements>

These modules allow us to deploy all new page layouts to the 14 folder and ghost them in the master page gallery. Existing layouts created with SharePoint Designer, however, explicitly live in the content database, and are represented by fully fledged SPFile objects in the gallery. But by giving these a module as well, we have the flexibility to _layouts-deploy them to 14 should the client, in an ideal "version one" phase, allow us to blow the page layouts away.

But what about overwriting existing layouts that already live in the gallery, and can't be moved or deleted and re-created by Visual Studio? This is where the code comes in. We are going to create a feature with a receiver that analyzes the modules in our WSP, and for all pages, contains logic to decide how to update them (again, without disrupting any existing pages provisioned from them). Let's look at the code:

  1. #region Events
  2. public override void FeatureActivated(SPFeatureReceiverProperties properties)
  3. {
  4. //deploy master page and page layouts
  5. SPSite site = (SPSite)properties.Feature.Parent;
  6. this.DeployMasterPageAndPageLayouts(site, properties.Definition.GetElementDefinitions(CultureInfo.InvariantCulture));
  7. }
  8. public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  9. {
  10. //not supported
  11. }
  12. #endregion
  13. #region Private Methods
  14. private void DeployMasterPageAndPageLayouts(SPSite site, SPElementDefinitionCollection elements)
  15. {
  16. //initialization
  17. SPWeb web = site.RootWeb;
  18. //get all modules
  19. foreach (SPElementDefinition module in elements.OfType<SPElementDefinition>().Where(x => x.ElementType.Equals("Module", StringComparison.InvariantCultureIgnoreCase)))
  20. {
  21. //these are files
  22. foreach (XmlNode file in module.XmlDefinition.ChildNodes)
  23. {
  24. //make sure we are dealing only with ghostable pages
  25. if (file.Attributes["Type"].Value.Equals("GhostableInLibrary"))
  26. {
  27. //get the file attributes
  28. string url = file.Attributes["Url"].Value;
  29. string path = file.Attributes["Path"].Value;
  30. url = string.Format("{0}/{1}", module.XmlDefinition.Attributes["Url"].Value, url);
  31. path = string.Format("{0}\\{1}", module.XmlDefinition.Attributes["SetupPath"].Value, path);
  32. //get the file
  33. SPFile layout = web.GetFile(url);
  34. switch (layout.CustomizedPageStatus)
  35. {
  36. //file is customized: it lives in the content DB [assumably created originally from SPD; will always be in gallery]
  37. case SPCustomizedPageStatus.Customized:
  38. layout.RevertContentStream();
  39. this.UpdateFileBitsInDatabase(layout, path);
  40. break;
  41. //file has never been cu