Sunday, 19 August 2007

Automatically setting custom permissions on new sites

This is the third and final article in a series of three, where I demonstrate how how to perform custom processing in the site creation process. See 'Article series - custom permissions with a site definition' for the full run down on the article series. Specifically, I wanted to show how to use code to modify sites as they are created, in order to do things which aren't normally possible with site definitions/site templates. In the example I'm using, I'm setting custom permissions on the created sites. A scenario where this might be useful is if say, your organization is using SharePoint in a collaboration sense and users are creating sites themselves, but certain sites need to be secured so that access is restricted to specific users. Often end users might not understand the details of the SharePoint security model, so it would be nice if we could take care of this automatically for them.

The solution

In the last article 'Site definitions - custom code in the site creation process', I showed how it's possible to use a Feature receiver in conjunction with the site definition to do pretty much anything you might want to do as sites are created. Based on this approach, my solution is based around the following:



  • Custom list which stores the list of authorized users in the site collection's root web. This list stores a mapping of users to the permissions they should have in the created site.
  • Custom site definition, created by copying an existing definition as described in the SDK.
  • Feature which doesn't have any Feature elements defined, but is attached to a Feature receiver. A property is defined to pass in the name of the permissions list.
  • Feature receiver code which uses the object model to iterate the permission list and grant appropriate permissions to each user listed.

So let's break down each element of the solution. Note that all the code etc. is available for download and is linked to at the end of the article. First of all, we create a list which looks like this (click to enlarge):



Looking at the the edit view for the list (shown below), we see that the two key columns are:

  • 'User' - Person or Group data type
  • 'Permission level' - choice data type, with allowable values 'Owner', 'Contributor' and 'Viewer'




If we are creating sites which are restricted we would probably want to secure the list so that curious users cannot add themselves, and then gain access to any future restricted sites which are created.


So that's the list. The site definition in my example doesn't do anything special - it's just a copy of the 'BLANKINTERNET' definition to keep the example simple. However, 'Creating, deploying and updating custom site definitions' has more information on the kinds of customizations you can make with your site definitions.

The Feature is defined to reference the Feature receiver class we are creating. This ensures our custom code will run when the Feature is activated.

<Feature  Title="SiteProvisioning" Id="7C020FFF-FF42-4fe2-8A9B-9BCA0D5F8001" Description="" Version="1.0.0.0" Scope="Web"

          Hidden="TRUE" DefaultResourceFile="core"

          ReceiverAssembly="COB.Demos.SiteDefinition, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cd9b418c14cff42e"

          ReceiverClass="COB.Demos.ProjectXSiteDefinition.SiteProvisioning" xmlns="http://schemas.microsoft.com/sharepoint/">

  <Properties>

    <!-- could also retrieve list by GUID by passing in as property and amending code slightly -->

    <Property Key="PermissionsListName" Value="Project X Permissions" />

  </Properties>

</Feature>


The 'ReceiverAssembly' and 'ReceiverClass' attributes have the values which point to our Feature receiver class which contains our custom code. Note also we are passing in a value which can be retrieved in the code by using a Feature property. This can be used as a more flexible alternative to hardcoding values in the class - in this case we are using it pass in the name of the 'authorized users' list, meaning that this Feature can be reused across different requirements (which would use different lists). A minor tweak to the code and property will allow you to use the list GUID if you prefer, though note that list GUIDs will be different if the list is recreated in another SharePoint environment.

So that's all great but what ensures the Feature gets activated? Ah you've lost the thread since the last article haven't you?! The Feature is activated automatically when sites are created using the definition courtesy of this line in the WebFeatures section of the onet.xml file which specifies what the site definition consists of:


<Feature ID="7C020FFF-FF42-4fe2-8A9B-9BCA0D5F8001">


And so finally, onto the code which we have written in our Feature receiver class:

using System;

using Microsoft.SharePoint;

 

namespace COB.Demos.ProjectXSiteDefinition

{

    class SiteProvisioning : SPFeatureReceiver

    {

        public override void FeatureActivated(SPFeatureReceiverProperties properties)

        {

            SPWeb currentWeb = null;

            SPSite currentSite = null;

            object oParent = properties.Feature.Parent;

 

            // retrieve the permissions list by name..

            string sPermsListName = properties.Definition.Properties["PermissionsListName"].Value;

 

            // only perform processing if the site definition is being used to create a web within the expected site collection..

            if (properties.Feature.Parent is SPWeb)

            {

                currentWeb = (SPWeb)properties.Feature.Parent;

                currentSite = currentWeb.Site;

 

                SPList permsList = currentSite.RootWeb.Lists[sPermsListName];

 

                // ensure the web is set to use unique permissions, we won't copy existing permissions from parent site..

                if (!currentWeb.HasUniqueRoleAssignments)

                {

                    currentWeb.BreakRoleInheritance(false);

                }

 

                foreach (SPListItem perm in permsList.Items)

                {

                    string sPermLevel = (string)perm["Permission level"];

 

                    SPFieldUserValue userValue = (SPFieldUserValue)perm.Fields["User"].GetFieldValue(perm["User"].ToString());

                    SPUser user = userValue.User;

                    setPermission(currentWeb, user, sPermLevel);

                }

 

                currentWeb.Update();

            }

        }

 

        private void setPermission(SPWeb currentWeb, SPUser user, string sPermLevel)

        {

            SPGroup permissionsGroup = null;

 

            switch (sPermLevel)

            {

                case "Owner":

                    permissionsGroup = currentWeb.AssociatedOwnerGroup;

                    break;

                case "Visitor":

                    permissionsGroup = currentWeb.AssociatedVisitorGroup;

                    break;

                case "Member":

                    permissionsGroup = currentWeb.AssociatedMemberGroup;

                    break;

                default:

                    throw new NotImplementedException(string.Format("Group '{0}' not yet implemented.", sPermLevel));

                    break;

            }

 

            permissionsGroup.AddUser(user);

      }

 

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)

        {

        }

 

        public override void FeatureInstalled(SPFeatureReceiverProperties properties)

        {

        }

 

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)

        {

        }

    }

}


Stepping through the code, we first find the 'authorized users' list, tell SharePoint we don't want to inherit permissions for the web being created, and then iterate through the list adding each user to the appropriate security group for the web as we find them. Note the SPWeb object has properties to allow you to easily reference the 'Owners', 'Visitors' groups etc. - these will be named in the form '[My site name] Owners' so this avoids you having to do any nasty string concatenation here.

In terms of how where this class fits alongside the rest of the files, I just store it in the same VS project. In this example I'm not using VSeWSS to create the Solution, but as I mentioned last time this can make things much simpler. I'm choosing not to here because I wanted to pass the list name using a Feature property, and in the current version VSeWSS does not have the flexibility to support this. In any case, having the class in the same VS project means that when the project is compiled, the receiver assembly is built and is output to the same project's bin directory. My .ddf file which is passed to makecab.exe then adds this dll and all the other files to the Solution package (.wsp) which is built for deployment. You may choose to use a post-build type solution (MSBuild, post-build script etc.) to automatically deploy this to your local environment on every compile, either by straight XCOPY or using the STSADM commands - my 'Building and deploying SharePoint Solution packages' article has more information on this. So my overall project structure looks like this:




So for deployment this means everything is in one package - on deployment the assembly hits the GAC before the Feature activation process runs, meaning the Feature receiver code is in place and will execute successfully. Once deployed, the site definition is available for use and new sites can be created from it. If we go ahead and create a site, if we look at the different security groups we see the appropriate users have been added according the configuration data we stored in the 'authorized users' list.

Owners group:



Visitors group:


So that's it! We now have our solution which enables us to 'package up' custom permissions with a site definition. Clearly we could store the permissions mappings in some other store such as a database table or XML file, but all things being equal I'm a big advocate of using SharePoint lists to store such data. The user interface is provided for you, and security can be applied to ensure standard users are unaware of the list's presence.

All the files I used can be downloaded from http://sharepointchris.googlepages.com/sitedefinitionwithcustompermissions.

In terms of using the files, you can follow this process to use the technique:

  • create the authorized users list from the list template I supply - 'PermissionsListTemplate.stp'
  • add your users and permission mappings to the list
  • add your site definition files to the appropriate places in the VS project, and modify the onet.xml file etc. as necessary
  • if files have been added, amend the .ddf file to include these and rebuild the Solution package
  • deploy the package using STSADM (a .bat file is included in the zip) and create sites from the definition!

Hopefully this series has been some use. While the approach is certainly useful in my scenario of rolling out a site definition used to create automatically secured sites from, in general terms you can use the technique to do any custom processing you want in the site creation process. If there are any queries please leave a comment!

Sunday, 12 August 2007

Site definitions - custom code in the site creation process

This is the second article in a series of three, where I aim to show how to customize the site creation process (known as site provisioning) with your own API code. The full introduction and series contents can be found at http://sharepointnutsandbolts.blogspot.com/2007/07/article-series-custom-permissions-with.html. The example customization I'm using is as follows: any sites created with the definition should use a specific set of permissions, and not simply follow the default behavior of inheriting the parent site's permissions. Since this can't be done with a standard site definition (like many other things you might want to do), use of the API is required.

However, today the focus is less on the permission specifics of my example, and more on how generally to add your own code which runs in the site provisioning process. And the best thing is, it's actually very simple if you understand SharePoint Features.

There are many reasons why you might have cause to use the API in the site provisioning process. Essentially, if you can't find a way to do what you want using CAML schema in the onet.xml file, chances are you'll have to write code. Hence, it's almost easier to think of what you can do in the onet.xml file and reverse the list in order to work out scenarios which require code, but some examples which spring to mind anyhow are:

  • changing the custom master page of a site
  • creating a site column which gets it's data from a list (see my post on my Feature receiver which does this at Feature to create lookup fields on Codeplex)
  • adding custom unique permissions to a site (the example in this article series)
  • set a site property from any kind of dynamic lookup

In short, there are many scenarios.


Creating site definitions with VSeWSS

If you've ever created a site definition with Visual Studio Extensions for Windows SharePoint Services, you'll notice that the VS project it gives you contains a file called SiteProvisioning.cs. Inside is an event-handler method, where you can add your custom code which will execute when a site is created from the definition. The class looks like this:

namespace COB.Demos.SiteDefinition

{

    public partial class ProjectXSiteDefinition

    {

        /// <summary>

        ///  Define your own feature activation action code here

        /// </summary>

        public void OnActivated(SPFeatureReceiverProperties properties)

        {

            // my code here..

        }

    }

}

 

The plumbing behind all this is interesting. At first glance, the method signature looks like a Feature receiver, but it's actually not. However, examining the VS project (you'll need to build the project with F5 at least once to generate the files) reveals that VSeWSS has in fact created some Features in the background. These files can be found under the bin\Debug\solution folder in your VS project (hidden by default - you'll need to do a 'Show All Files' in Visual Studio Solution Explorer). If you do some more delving around to see exactly what VSeWSS is doing, you'll find the following:

  • 2 hidden Features have been created - 1 deploys the 'default.aspx' file, the other has no 'elements' file but is hooked up to a Feature receiver - this is a class in an assembly named the same as your VS project. If you check the GAC, you will indeed find this assembly there.
  • a line similar to the following has been added to the onet.xml file under the 'WebFeatures' element:

    <Feature ID="67b2507c-8822-41dc-b939-3d8f34b5ad13" />


    Notably, this is the ID of the Feature which is hooked up to the Feature receiver.
  • Using Reflector on the assembly containing the Feature receiver shows that the main event-handler method performs some processing and then calls into the OnActivated method shown above, i.e. the place where VSeWSS provides for you to add your own code to execute when sites are created. This code is actually contained in the SiteProvisioning.Internal.cs file within the VS project. (If you're curious as to what on earth all the code in here is doing, the answer as far as I can tell is nothing when site definitions are created with the VSeWSS project template. However, this code is also found when Solution Generator is used to extract a site definition - in that case there are some fixups which need to be done, and this is the code which is used.)

So in summary, VSeWSS creates a hidden Feature is added to the 'WebFeatures' section of the onet.xml so that it is automatically activated when the definition is used to create a web*. The Feature is hooked up to a Feature receiver which calls the OnActivated method where your custom code lives.

*(Note that if the definition is used to create a site definition, the root web is also created automatically so the Feature would also be activated then. Also note the feature needs to be already installed in the farm for it to be activated in this way).

What we can derive from this is that there's no 'special place' in the site provisioning process to inject custom code, but it can be accomplished by use of a Feature receiver. So if you don't want to use VSeWSS to create site definitions, this is the technique to use to add your custom code to the site creation process.

In terms of what that code might look like, a 'Hello World' example could be:

public void OnActivated(SPFeatureReceiverProperties properties)

{

     SPWeb currentWeb = null;

     SPSite currentSite = null;

     object oParent = properties.Feature.Parent;

 

     if (properties.Feature.Parent is SPWeb)

     {

         currentWeb = (SPWeb)oParent;

         currentSite = currentWeb.Site;

     }

     else

     {

         currentSite = (SPSite)oParent;

         currentWeb = currentSite.RootWeb;

     }

 

     currentWeb.Title = "Set from provisioning code at " +  DateTime.Now.ToString();

     currentWeb.Update();

}


Hopefully this illustrates that it's quite simple to write code which sets properties on sites created from the definition. Generally the SPWeb object is the entry point, and any property which can be modified can be modified using the API. So, this is a pretty powerful technique which can be used in many scenarios.

If you have this type of requirement, I'd definitely recommend using VSeWSS to simplify the process. It's certainly possible to hook everything up manually and package it into a Solution, but the tool does save a large amount of hassle. However as usual with VSeWSS, the price of this is some flexibility. As my sample code in the final article will show, it's sometimes useful to pass data into Features by using Feature properties, and this unfortunately is not supported by VSeWSS. So in case it's useful, the following link provides a zip file containing a Solution/Feature which uses the above technique, without using VSeWSS:

http://sharepointchris.googlepages.com/customcodewithsitedefinitions

In the next and final article, I'll cover the specifics of using the API to modify site permissions as sites are created. As is hopefully clear, this is in conjunction with the technique detailed here so the net result is that the specific permissions are set 'automatically', courtesy of the Feature which is automatically activated against a site when it is created.

Sunday, 5 August 2007

Creating, deploying and updating custom site definitions

This is the first article in a series of three where we'll discuss custom site definitions, and in particular how to run your own custom code in the site creation process. This technique is useful if you need to make any customizations using the API beyond what can normally be accomplished with a site definition. In my series (for the full series contents, see my introduction at http://sharepointnutsandbolts.blogspot.com/2007/07/article-series-custom-permissions-with.html), I use the example of creating a site definition with specific security permissions 'attached' - so that when any sites are created using the definition, specific permissions are applied which are different to those of the parent site (this is unlike the default, which is for new sites to inherit the parent site's permissions). More background can be found in the introductory article linked to earlier.

In this article we'll start with the site definition basics - I'll also supply the set of files used in this article in a link at the end. Fundamentally, a custom site definition is a template from which new SharePoint sites can be created. Customizations can be packaged into the definition, so that they are present automatically in sites created from the template. Consider the following about site definitions:

  • they are created by copying an existing (e.g. out-of-the-box) site definition and adding customizations
  • XML files (in particular the onet.xml file) specify what a site definition consist of (i.e. .aspx pages, images, web parts, functionality [in the form of SharePoint Features])
  • they provide a similar functionality to site templates (.stp files) - for a discussion of the differences between site definitions and site templates see http://msdn2.microsoft.com/en-us/library/aa979683.aspx
  • site definitions can be deployed using a SharePoint Solution package (.wsp file), so that files do not need to be manually copied to each web server in a SharePoint farm

The process for creating site definitions is well-documented in the WSS 3.0 SDK at http://msdn2.microsoft.com/en-us/library/ms454677.aspx, so I actually won't cover it here but would encourage you to follow the link. However I will quickly run through some of the key elements in the onet.xml file to go over what can be done with site definitions. For an example, let's take an extract of the onet.xml file for a publishing (note for clarity this is an extract only - a full version is available at the link at the end of the article):

<Configuration ID="0" Name="BLANKINTERNET">

  <SiteFeatures>

    <Feature ID="A392DA98-270B-4e85-9769-04C0FDE267AA">

      <!-- PublishingPrerequisites -->

    </Feature>

    <Feature ID="7C637B23-06C4-472d-9A9A-7C175762C5C4">

      <!-- ViewFormPagesLockDown -->

    </Feature>

    <Feature ID="F6924D36-2FA8-4f0b-B16D-06B7250180FA">

      <!-- Office SharePoint Server Publishing -->

    </Feature>

  </SiteFeatures>

  <WebFeatures>

    <Feature ID="00BFEA71-4EA5-48D4-A4AD-305CF7030140" > </Feature>

    <Feature ID="22A9EF51-737B-4ff2-9346-694633FE4416">

      <!-- Publishing -->

      <Properties xmlns="http://schemas.microsoft.com/sharepoint/">

        <Property Key="ChromeMasterUrl" Value="~SiteCollection/_catalogs/masterpage/BlueBand.master"/>

        <Property Key="WelcomePageUrl" Value="$Resources:cmscore,List_Pages_UrlName;/default.aspx"/>

        <Property Key="PagesListUrl" Value=""/>

        <Property Key="AvailableWebTemplates" Value="*-ProjectX#0"/>

        <Property Key="AvailablePageLayouts" Value="ThreeColumnLayout.aspx"/>

        <Property Key="AlternateCssUrl" Value="" />

        <Property Key="SimplePublishing" Value="false" />

      </Properties>

    </Feature>

  </WebFeatures>

  <Modules>

    <Module Name="LoginPage" />

    <Module Name="Images" />

    <Module Name="Home" />

  </Modules>

</Configuration>


  • The Configuration element represents settings to be used with the selected definition, allowing groups of settings to be reused across definitions for flexibility
  • SiteFeatures /WebFeatures - specifies which Features should be automatically activated when the definition is used to create a site collection or child site respectively. This is key to our overall aim in this series of creating a site definition which applies custom unique permissions to sites created from it.
  • AvailableWebTemplates property of publishing feature - can be used to restrict which site definitions can be used to create child sites within sites created from this definition. This can be useful to prevent your content creators adding a team site onto your public-facing .com website for example. Here I'm specifying that only the 'ProjectX' definition with Configuration '0' can be used for child sites.
  • AvailablePageLayouts property of publishing feature - can be used to restrict which page layouts can be used within sites created from this definition. Again, this can be useful in controlling the look and feel of your website.
  • Modules - a module is a set of files to be automatically added when sites are created. Note it's also possible to specify which web parts should be added by default to a web part page by using the AllUsersWebPart element.


Deploying site definitions


What I really want to focus on however, is how site definitions can be packaged into a Solution to simplify deployment. This is typically most useful when deploying to multiple environments (e.g. test, staging, production) and/or deploying to a farm which consists of multiple SharePoint web servers. If you don't have this requirement, you may want to consider the simpler process of copying the XML files around manually, as detailed in the WSS SDK.

Before we start, let me highlight that Visual Studio Extensions for Windows SharePoint Services (VSeWSS) is a useful tool for creating and deploying custom site definitions, and I'd recommend looking into it for this requirement if you haven't already. However, I'm illustrating the 'manual' way here to hopefully provide an understanding of the nuts and bolts.

So, to package these files as a Solution manually, we follow the SDK instructions to get our customized onet.xml and webtemp*.xml files, but then place the files in a similar folder structure to the existing site definition files found under the 12 folder. I recommend creating a Visual Studio project as the best way to group these files (VSeWSS also follows this approach). This means you should end up with something looking like this:



Note we also have a .ddf file which is used with makecab.exe to build the Solution package - see my article on building and deploying Solution packages for details on the full process here. Effectively, we need to build the Solution by passing the .ddf file as a parameter to makecab.exe, and then run the STSADM -o addsolution and STSADM -o deploysolution commands to deploy to our target SharePoint site.

Once this is done, on the 'create site' screen we should see our new template appear:



Users can now use this definition to create sites which automatically have all the functionality and appearance we specified upfront. If we then need to deploy the definition to other environments, it's a simple case of copying the .wsp file there and running the STSADM commands. We are now well on our way to creating a site definition with custom permissions associated with it.

The set of files used in this article are at http://sharepointchris.googlepages.com/creatinganddeployingcustomsitedefinition

Updating existing site definitions


Finally, a quick note on updating definitions. Care needs to be taken here as often you will want to update files which are in use by sites already created with the definition - this can break things! Generally adding to a definition is OK, but modifying/deleting things can cause problems.

So a good technique is to copy the existing definition, add the updates and deploy for new sites to use, but also hide the earlier version so it cannot be used going forward. This is accomplished by removing the webtemp*.xml file from the TEMPLATE\\XML directory. Any sites already provisioned from the earlier version of the definition will continue to run fine, since you're leaving the actual definition (onet.xml etc.) intact over in TEMPLATE\SiteTemplates.

Remember also that new Features can be stapled to existing site definitions (affecting only new sites which are created, not existing ones), and this can be useful in avoiding having to update the site definition itself. See my article on Feature-stapling for more details.

So that's site definition basics. Next in the series - how to go beyond simple site definitions : add custom code which will execute when sites are created!

Tuesday, 31 July 2007

Article series - custom permissions with a site definition

This is an introductory article which gives the background to an interesting problem I've seen recently. Over the next 3 articles, I'll be looking at a solution to the problem and will discuss the following:

What I aim to show is how to deploy a site definition which has custom permissions 'attached' to it. By this I mean that any sites created with this template will acquire a set of custom permissions, without a user/admin having to configure them separately. What we're effectively doing is automatically putting a set of users into SharePoint site groups (at the time of site creation) - assuming the site was called 'HR', these would be:

  • HR owners
  • HR members
  • HR visitors

This can be extremely useful in certain scenarios when users are creating their own sites and workspaces in SharePoint, i.e. collaboration scenarios. One example might be a situation where say, the senior management of your organization (who are all fairly familiar with SharePoint of course ;-)) want to create sites for an important initiative they have. They want to do this on an ad-hoc basis without involving IT, but the sites must be restricted access such that only certain people can use them. Additionally, the requirements might be that some people have full control, but others can only read.

Since these security requirements are probably different to those of the parent site collection, the site must implement 'unique permissions'. This is key - if the site can share (and that's share, not inherit - think about the difference) the permissions of the parent site, then custom permissioning is not required. Otherwise if you are happy to set up the custom permissions manually each time a site is created, that can be done simply too without a solution like mine. SharePoint provides dialog screens for this during the site creation process, if you select 'Use unique permissions' in the Permissions section.

However, when it should happen automatically, you need to perform some custom processing during the site provisioning process, which is what I'm illustrating here. So with the background out of the way, the articles will consist of the following:

Article 1 - Site definition basics

  • Process for creating custom site definitions
  • How to deploy using a SharePoint Solution package (.wsp)

Article 2 - Custom code in the site provisioning process

  • When this is necessary
  • How to get your code to run at this time

Article 3 - how to implement unique permissions for sites created with the definition

  • Storing permissions data in a secured SharePoint list
  • Feature Receiver code to apply the permissions

Article 1 in the series should be published in a couple of days. See you then!

Sunday, 22 July 2007

Building and deploying SharePoint Solution packages

Following a comment left elsewhere on this blog, I thought I'd cover exactly what SharePoint Solution packages are, including how they are built and deployed to other SharePoint environments.

First of all, let's be clear on exactly what a Solution package is. Note I'm using 'Solution' with a capital S  in this article to distinguish between what we are discussing here and the general idea of a technical solution built using SharePoint. A Solution is a package of SharePoint items for deployment - in physical terms it is a cabinet file (.cab) which is given the extension of .wsp to differentiate it from standard .cab files. Other articles on this blog cover the idea of using SharePoint Features to deploy functionality, so let's also be clear on the relationship between Features and Solutions. In general terms, some of the tasks which cannot be done with a Feature alone but can be done with a Solution include:

  • Deployment of certain files to the filesystem, e.g. an assembly for workflow or web parts, custom files which will reside in the ‘12’ folder 
  • Deployment of web part definition files (.webpart)
  • Web.config modifications e.g. the ‘SafeControls’ entry required for a custom web part
  • Code Access Security config modifications e.g. those required for custom web parts not running from the GAC

In addition to being able to do these things, Solutions can also contain Features. The way I think of it is that the Solution wraps the Feature. In actual fact, I always recommend that even if they don't need to be (e.g. we're not doing anything in the list above), Features are always deployed as part of a Solution.  The reason for this is that the Solution framework takes care of deploying all required files to all Web Front End (WFE) servers in a SharePoint farm. This alone is incredibly useful in working towards repeatable deployments, and ensures your WFEs stay synchronized.

How Solutions are specified

The key file used to specify what a Solution package consists of is the manifest.xml file. Going back to my earlier post on deploying web parts, the manifest.xml file for that scenario looks like this:

<Solution xmlns="http://schemas.microsoft.com/sharepoint/" SolutionId="122C0F04-78B7-4d42-9378-6F8B4F93ADD1">

  <FeatureManifests>

    <!-- note this is the location in the cab file! -->

    <FeatureManifest Location="COB.Demo.WebPartDeployment.WriteToFileWebPart\feature.xml" />

  </FeatureManifests>

  <Assemblies>

    <Assembly     Location="COB.Demo.WebPartDeployment.WriteToFileWebPart\COB.Demo.WebPartDeployment.WriteToFileWebPart.dll"

        DeploymentTarget="GlobalAssemblyCache">

      <SafeControls>

        <SafeControl Assembly="COB.Demo.WebPartDeployment.WriteToFileWebPart, COB.Demo.WebPartDeployment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9f4da00116c38ec5"

                    Namespace="COB.Demo.WebPartDeployment"

                    Safe="True"

                    TypeName="*" />

        </SafeControls>

    </Assembly>

  </Assemblies>

  <DwpFiles>

    <DwpFile

     Location="COB.Demo.WebPartDeployment.WriteToFileWebPart\COB.Demo.WebPartDeployment.WriteToFileWebPart.webpart"

      FileName="COB.Demo.WebPartDeployment.WriteToFileWebPart.webpart" />

  </DwpFiles>

</Solution>


This is assuming the web part is being deployed to the GAC - for this illustration this is a simpler scenario than deploying to the web application's bin directory, where Code Access Security (CAS) policy would also be required. Effectively, the manifest specifies that the Solution package consists of the following:

  • a Feature with a header file named 'feature.xml'
  • an assembly for deployment to the GAC named 'COB.Demo.WebPartDeployment.WriteToFileWebPart.dll'
  • an entry in the SafeControls section of the application's web.config file, specifying all types in the specified assembly should be treated as safe
  • a web part definition file named 'COB.Demo.WebPartDeployment.WriteToFileWebPart.webpart' - this will be deployed to the web part gallery on the site

Importantly, all these details in the manifest.xml file are only used by SharePoint when the generated Solution package is deployed. They are effectively the instructions which say "go and get this item from the .wsp file and put it here". The actual process for generating the .wsp file is another task.


Building Solution packages

There are 2 options for building the actual package - build it manually using makecab.exe, or use an automated solution - there are several community-developed tools/techniques available. Since it's always good practise to understand what's actually happening in such processes, we'll cover how to do it manually here.

The first step is to write a .ddf (Diamond Directive File). This is a set of instructions for makecab.exe on how to build the folder hierarchy inside the .cab file. For my web part example, my file looks like:

.OPTION Explicit

.Set CabinetNameTemplate="COB.Demo.WebPartDeployment.WriteToFileWebPart.wsp" 

.Set DiskDirectory1="Package"

;***

 

manifest.xml

 

;** this directory name is used for the folder name under 12\TEMPLATE\Features, so should

;** match up with what you want to call the feature!

.Set DestinationDir=COB.Demo.WebPartDeployment.WriteToFileWebPart

elements.xml

feature.xml

 

.Set DestinationDir=COB.Demo.WebPartDeployment.WriteToFileWebPart

WebPart\COB.Demo.WebPartDeployment.WriteToFileWebPart.webpart

WebPart\COB.Demo.WebPartDeployment.WriteToFileWebPart.dll

 

;***


This file tells makecab.exe to do the following:

  • create a .cab file named 'COB.Demo.WebPartDeployment.WriteToFileWebPart.wsp'
  • put the 'manfest.xml' file at the root of the hierarchy
  • put the 'elements.xml' and 'feature.xml' files in a subfolder called 'COB.Demo.WebPartDeployment.WriteToFileWebPart'
  • also put the 2 other files (with extensions .webpart and .dll) in this same subfolder, but that makecab.exe should look for these files in a subfolder on the main filesystem called 'WebPart'

This instructions file is then passed to makecab.exe with the following command-line command:

D:\SolutionDeployment\Development\COB.Demo.WebPartDeployment>"C:\Program Files\Microsoft Cabinet SDK\BIN\MAKECAB.EXE" /f COB.Demo.WebPartDeployment.ddf


Couple of things to note here. At the command prompt we have ensured the current directory is the directory where all our Solution files are kept. This ensures that our relative references in the .ddf file above can be resolved. We pass the /f parameter to indicate we are passing a directives file, and also pass the location of this file.

Assuming all the files/references are OK, this results in a .cab file with a .wsp extension which will be created in a subfolder under the current directory called 'Package' (the folder will be created for you if it doesn't exist). We now have a Solution package which can be deployed to another SharePoint server. But first let's take a peek inside - this can be done by temporarily renaming the extension to .cab. You should then see something like:



Deploying the Solution to other SharePoint servers

The final part is to actually deploy the Solution. First the .wsp file must be copied to the target server, and then we will use STSADM (SharePoint's command-line admin tool) to actually deploy the Solution. The following are the commands which used to perform the deployment:

stsadm -o addsolution -filename COB.Demo.WebPartDeployment.WriteToFileWebPart.wsp

stsadm -o deploysolution -name COB.Demo.WebPartDeployment.WriteToFileWebPart.wsp
   -url http://myWebApplication -immediate -allowGacDeployment -allowCasPolicies -force


The first command simply adds the Solution package to SharePoint's Solution store in the config database, and the second one actually performs the deployment of the package to the specified web application. These areas are fairly well covered in the SDK at http://msdn2.microsoft.com/en-us/library/aa544500.aspx.

Rather than write these commands at the command-line each time you wish to deploy the package, chances are over time you'll package these commands up into STSADM scripts which do the work. As an example, the script I use for the webparts example looks like:

:begin

@echo off

 

set solutionName=COB.Demo.WebPartDeployment.WriteToFileWebPart

set url=http://myWebApplication

set featureName=COB.Demo.WebPartDeployment.WriteToFileWebPart

@set PATH=C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\BIN;%PATH%

 

echo --- Attempting to deactivate/retract existing solution...

 

stsadm -o deactivatefeature -name %featureName% -url %url% -force

stsadm -o retractsolution -name %solutionName%.wsp -url %url% -immediate

stsadm -o execadmsvcjobs

stsadm -o deletesolution -name %solutionName%.wsp -override

rem stsadm -o execadmsvcjobs

 

echo --- Adding solution %solutionName% to solution store...

 

stsadm -o addsolution -filename %solutionName%.wsp

 

if errorlevel == 0 goto :deploySolution

 

echo ### Error adding solution %solutionName%

echo .

goto end

 

:deploySolution

echo --- Deploying solution %solutionName%...

 

stsadm -o deploysolution -name %solutionName%.wsp -url %url% -immediate -allowGacDeployment -allowCasPolicies -force

stsadm -o execadmsvcjobs

 

if errorlevel == 0 goto :activateFeature

 

echo ### Error deploying solution %solutionName%

echo .

goto end

 

:activateFeature

 

echo --- Activating features in solution %solutionName%...

 

stsadm -o activatefeature -name %featureName% -url %url% -force

 

if errorlevel == 0 goto :success

 

echo ### Error activating feature %featureName%

echo .

goto end

 

:success

echo Successfully deployed solution and activated feature(s)..

echo .

goto end

 

:end

pause


Following the script through, this basically does the work of safely upgrading the Solution (in case it has been deployed previously), and also activating the Feature within the Solution.


Automating Solution builds

Earlier, we mentioned the idea of automating the process of building and deploying Solutions. There are several community tools/ideas in this area, a couple of the ones I'm aware of are:

Both of these effectively use Visual Studio post-build events to automate the build of the .wsp package.

Additionally, Microsoft's Visual Studio Extensions for Windows SharePoint Services effectively performs the same tasks. Although there is some inflexibility in the current version, this add-on for VS means that the whole process is automated by hitting the F5 button. I'd recommend taking a look if you're not aware of this. My post on creating lists with VSeWSS gives an overview of using it for that specific scenario.

Wednesday, 18 July 2007

Using CustomAction to modify system pages

In the last post, I covered 2 examples of how to safely modify functionality in 'system' areas of SharePoint, such as Central Admin and the Site Settings area. The first example was a tweaked Recycle Bin (which listed only items deleted by the current user), and the second was a custom 'Create Site Collection' admin page. In both cases, the technique was to copy the original page which shipped with SharePoint, make the customization to the copy, and then redirect the link in SharePoint to use our new page. The customization is then fairly transparent to users - depending on your implementation, they could simply be clicking the same link in the same place in SharePoint, yet going to somewhere different which is offering your modified functionality.

This last part (of redirecting the link in SharePoint) is accomplished via a Feature which makes use of the CustomAction element. However, this idea of changing an existing link somewhere in SharePoint is not necessarily the type of customization you might regularly make. So it's useful to know the CustomAction element can also be used to add a link somewhere in SharePoint. This opens up many opportunities for customization, and this post aims to detail some of the options.

The CustomAction element is used like this:

<CustomAction Id="MyDeletedItems"

              GroupId="SiteCollectionAdmin"

              Location="Microsoft.SharePoint.SiteSettings"

              Sequence="10"

              Title="My recycle bin items"

              Rights="ManageWeb,BrowseUserInfo">

  <UrlAction Url="_layouts/custom/MyRecycleBinItems.aspx" />

</CustomAction>



I covered most of the values in details in the previous post, but what I want to focus on here is the Location attribute - this essentially specifies where in SharePoint your link will be added to. Some of the common potential values are:

  • Microsoft.SharePoint.SiteSettings (as in the example XML above)
  • Microsoft.SharePoint.Administration.Operations
  • Microsoft.SharePoint.Administration.ApplicationManagement

If you've worked with SharePoint for a while you can probably work out where these values refer to - the first is the Site Settings area, whilst the last two are the 'Operations' and 'Application Management' pages in the Central Admin area respectively. When using a CustomAction to add links in these areas, you can also specify the group your link should appear in. This value refers to an existing 'category' of links within the Location, e.g. the locations listed above. Note that new groups can be created by using a CustomActionGroup element, but the GroupID value of a CustomAction must always specify a group which exists already. The following groups exist by default in the SiteSettings area:

  • UsersAndPermissions
  • Customization
  • QuickLaunch
  • Galleries
  • SiteAdministration
  • SiteCollectionAdmin

These correspond to the columns in the Site Settings area as shown below:




In the Central Admin pages, examples for the 'Operations' page are:

  • Topology
  • GlobalConfiguration
  • Security
  • BackupRestore
  • LoggingAndReporting
  • DataConfiguration
  • Upgrade

And for the 'Application Management' page:

  • WebApplicationConfiguration
  • SiteManagement
  • ApplicationSecurity
  • ExternalService
  • WorkflowManagement

By examining these pages, it's fairly simple to work out where the values refer to.

I mentioned earlier that new groups of links can be created using the CustomActionGroup element. An example of this would be:

<CustomActionGroup

    Id="CustomSiteSettingsGroup"

    Location="Microsoft.SharePoint.SiteSettings"

    Title="New custom group"

    Sequence="100"

    Description="My description" />



This will then create a new group of links on the page with your custom title - SharePoint will take care of the rendering such that your new group will wrap onto another row of groups. Of course if this isn't what you want you'll probably have an interesting time customizing this.

Using CustomAction elsewhere

So having covered how to add links into the admin areas, I'll wrap up by listing some other valid values for the 'Location' attribute. This hopefully illustrates some of the options you have for customizing SharePoint in this way:
  • Microsoft.SharePoint.ContentTypeTemplateSettings
  • Microsoft.SharePoint.ContentTypeSettings
  • Microsoft.SharePoint.Administration.ApplicationCreated
  • Office.Server.ServiceProvider.Administration (Shared Services/SSP links)
  • Microsoft.SharePoint.ListEdit.DocumentLibrary
  • Microsoft.SharePoint.Workflows
  • NewFormToolbar
  • DisplayFormToolbar
  • EditFormToolbar
  • Microsoft.SharePoint.StandardMenu (SiteActions menu)
  • Mcrosoft.SharePoint.Create (_layouts/create.aspx - the screen used to specify what you want to create on your site)
  • Microsoft.SharePoint.ListEdit (the screen used to edit the properties of a list item)
  • EditControlBlock (image below)
I've put the last few in bold and added a description as I think they're particularly interesting. Of course, if you have other customizations in mind one or two of the others might have caught your eye! In particular being able to add custom actions to the EditControlBlock is interesting:




Here, a special attribute called 'RegistrationType' can be added to the CustomAction element, allowing you to control exactly the circumstances where your action should appear. Valid options include 'List', 'ContentType', 'FileType' and 'ProgId'. Vince (aka TheKid) has some good examples of this over on his blog.

Hopefully this has given you some ideas on how you can add or amend the SharePoint UI in this way. Happy customizing!

Thursday, 12 July 2007

Modifying 'system' pages in SharePoint safely - with sample code

In a presentation I gave recently I talked about modifying admin screens within SharePoint. By this, I mean areas such as the site admin area and the central admin area:

Site admin:


Central admin:



Now, if your boss (or your users) asks you to add or change functionality in these areas, how would you handle that?

Well, you'd probably start off by looking to see how SharePoint implements 'system' pages like this. You'd then find they are just standard .aspx files in the 12\TEMPLATE\ADMIN folder on the filesystem. You'd also find that if you make a change to such a file, the changes are immediately reflected on the site - sure, some of the code is in a compiled assembly which is obfuscated (so Reflector doesn't help), but otherwise there's no mysterious abstractions, no caching, it's that simple. So changing the functionality or adding a link is a matter of changing the .aspx page right?

The problem with this approach is that modifying core files in this way is unsupported. This is because when a service pack or hotfix gets applied, your system will be in an inconsistent state and at best your changes will be overwritten. So a better way to implement such changes is to:

  • take a copy of the original file
  • modify the copy to include your customization
  • create a SharePoint Feature (wrapped in a Solution package [.wsp]) to deploy your customized page to the 12 hive, and also add/modify the link in SharePoint to point to your customized page

The end result of this is that you have deployed your change without modifying any of the original files, and are therefore entirely supported. Additionally, because Features can be activated and deactivated with the click of a button, if you want to revert to the original functionality that's all it takes.

Let's look at an example - I'll link to all the code used here at the end of the article. Imagine your users are asking you to change the Recycle Bin screen so that it only displays items the currently logged on user has deleted - perhaps there's a manual document clean up exercise happening. And let's also say that after the exercise is complete, the users would like to revert back to the original functionality. (Now remember, what I'm demonstrating here is the mechanism for making customizations like this, so whilst this particular change might be unlikely in your organization, you might have other customizations which would help your users.)

So, first we need to modify the functionality of the Recycle Bin page to add the filtering. As mentioned earlier, we don't want to amend the original page, so we take a copy and modify that. In my example, I'm adding a script block to the code in front (since I don't have access to Microsoft's source code for the code-behind) to effectively perform the filtering at the UI level, which is fine for this example. Once we've created the page we want to use, we're ready to create the Feature and Solution to add our page to SharePoint.

Let's start with the manifest.xml file for the solution - this is important because this is how we actually deploy our custom .aspx page to the appropriate place within the SharePoint files. (Remember all this is available for download, the link is at the end of the article.)

<Solution xmlns="http://schemas.microsoft.com/sharepoint/" SolutionId="AA7EF934-C993-4f91-8AF4-0FB1D45927FA">

  <FeatureManifests>

    <FeatureManifest Location="COB.Demos.MyRecycleBinItems\feature.xml" />

  </FeatureManifests>

  <TemplateFiles>

    <TemplateFile Location="LAYOUTS\Custom\MyRecycleBinItems.aspx" />

  </TemplateFiles>

</Solution>


Notice what we are doing here is specifying that our custom .aspx page should be deployed to a 'Custom' subfolder within the 'LAYOUTS' directory. This is a good idea because it keeps our files separate from the core SharePoint files.

Then we have a fairly standard feature.xml file specifying the Feature identification info:

<Feature xmlns="http://schemas.microsoft.com/sharepoint/" Id="29D8B365-6024-4b1a-84D2-FF789FE56355"

        Title="COB.Demos.MyRecycleBinItems"

        Description="Adds a custom page to the Site Settings area to show items in the Recycle Bin which were deleted by the current user."

        Scope="Site" Hidden="FALSE"

        Version="1.0.0.0">

  <ElementManifests>

    <ElementManifest Location="elements.xml"/>

  </ElementManifests>

</Feature>


In this case we're specifying that our Feature is scoped at site collection-level, so we will modify all sites in the site collection.

Finally we have the our Elements file, which specifies what this Feature consists of. In this case, the key is the use of the CustomAction element - this can be used to add a link (i.e. an action) somewhere in SharePoint.

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

  <HideCustomAction Id="HideDeletedItems"

                    HideActionId="DeletedItems"

                    GroupId="SiteCollectionAdmin"

                    Location="Microsoft.SharePoint.SiteSettings" />

  <CustomAction Id="MyDeletedItems"

                GroupId="SiteCollectionAdmin"

                Location="Microsoft.SharePoint.SiteSettings"

                Sequence="10"

                Title="My recycle bin items"

                Rights="ManageWeb,BrowseUserInfo">

    <UrlAction Url="_layouts/custom/MyRecycleBinItems.aspx" />

  </CustomAction>

</Elements>


Let's walk through this file in detail. The first thing you notice is that we're using a 'HideCustomAction' element. You guessed it, this is used to hide a link in the SharePoint UI. Since we are effectively replacing a link, we actually need to hide the original one and supply our replacement, and this is done by using these 2 elements together. We assign the ID ourselves (if we want to refer to this 'HideCustomAction' elsewhere, this is the value we'll use), but all the other attributes of this tag must correspond to those specified for this action in the Feature Microsoft created to add the link to SharePoint in the first place. So in case you've not come across this yet, much of the original functionality in SharePoint is actually implemented using Features too. The way to find these values is to search in the 12\Template\Features folder. For this particular action, the most reliable thing to search for is the URL of the page, since any 'CustomAction' tag specifies the URL. Since we can find that the URL for the original Recycle Bin page is '/_layouts/AdminRecycleBin.aspx', searching for this finds the file containing the details of the original link. These are then used for our 'HideCustomAction' tag.

Next we specify our 'CustomAction' tag, again using many of the same values since we are replacing an existing action rather than adding a new one. The values we change are for the link title and URL, to point to our replacement page. Note also you can declaratively specify what permissions levels are required to see this link.

Once these files are made into a Solution package, deploying the Solution and activating the Feature has the following effects:

The link on the site settings page has changed from the original (on the left) to our replacement (on the right):


 A 'Custom' folder has been created in the '12\Layouts' directory and our custom .aspx page (MyRecycleBinItems.aspx) has been deployed to this folder. And of course, when a user clicks the link, the modified page has the functionality to only display items they have deleted:



The key thing of course, is that we've 'customized SharePoint functionality' without modifying any original SharePoint files - hence we are are 100% supported and can be confident our customizations won't cause any issues for service packs etc.

On the same lines, the sample stuff has a 2nd example - here we modify the 'Create Site Collection' page in Central Admin to display a guidance message to the SharePoint administrators. The idea is to encourage them to stop and think and perhaps consult the document containing the organization's policy on creating site collections:


Hopefully you might find this useful if you need to customize SharePoint in this way. The full set of files, plus the slides for the presentation I gave on this, are available at:

http://sharepointchris.googlepages.com/customizingsharepointsupportedway

In the next post, I'll be supplying some reference information which will help you make this type of modification in other areas of SharePoint. An important aspect of customizing SharePoint in this way is knowing exactly where and how the 'CustomAction' tag can be used, so that's what we'll look at.