Deploying Information Management Policy Using Feature Activation in SharePoint Server 2007
Summary: Learn how to operationally deploy policies through Microsoft Office SharePoint Server 2007 by using feature activation event handlers and the SharePoint Server object model. (8 printed pages)
Craig Schwandt, Microsoft Corporation
March 2009
Applies to: Microsoft Office SharePoint Server 2007
Contents
Introduction to Information Management in SharePoint Server 2007
Content Types and Policy Scope
Attaching Policy to Your Content Type
Working with Sealed Content Types
Modifying Existing Content and Content Types, and Provisioning for Future Sites
Workaround for FieldCollection Update Failure
Conclusion
Additional Resources
Download the code sample that accompanies this article: Information Management Policy Deployment Sample for SharePoint Server 2007
Introduction to Information Management in SharePoint Server 2007
Information management is a powerful aspect of Microsoft Office SharePoint Server 2007. Information management enables you to attach business processes to particular types of content by using declarative policy and procedural workflows, or a combination of both. Many types of policies are built into Office SharePoint Server, for example, bar codes, auditing, labels, and expiration. This part of the platform is also extensible through custom policies.
The information management policy is an extension to the core Windows SharePoint Services supplied as part of Office SharePoint Server 2007. The assembly that implements the feature is Microsoft.Office.Policy.dll.
Note
Information management policies are NOT included with Windows SharePoint Services 3.0; however, they are included in both Microsoft Office SharePoint Server 2007 Standard and Microsoft Office SharePoint Server 2007 Enterprise.
Policies are closely related to metadata. You can think of polices as metadata plus behavior. As such, policies are put to work when they are bound to content types, although policies can also live independently at the site collection level, before they are bound to content types. You can assign only one policy to a given content type or SharePoint list; however, that policy can contain any number of policy items.
Policies obey the inheritance principles of the parent-child relationship in the content type hierarchy. If a content type has a policy, all its children and progeny inherit that policy. You cannot directly change a policy in a child content type. This ensures that you can determine administratively that a policy is consistent even if child content types are created.
This article describes how to deploy policies operationally. This is an advanced topic that requires the use of SharePoint Features and some programming with the SharePoint object model. The example discussed in this article uses feature activation event handlers.
Scenario
Consider a scenario in which a company has its own publishing pages and a content type that defines company-specific metadata. The company wants to publish these pages for a duration that is set by the content author and, when the pages expire, to archive them automatically.
This scenario is a perfect case for using an expiration policy that is attached to the content type, but you cannot add the policy to the content definition. There is no way to put the policy declaration into the content definition; the policy must be attached to the content type either manually, through the user interface (UI), or through code by using the object model.
Another difficulty for this scenario involves applying changes to an existing content type, including attaching a policy, after the content type is deployed and documents based on it are created. This can be solved, but the solution can get complicated, depending on what changes are being made, and when they must be manifested. It is possible that every potentially affected item in the site might need to be visited.
Content Types and Policy Scope
As documented in Scoping and Updating Policies in the Microsoft Office SharePoint Server 2007 SDK, there are two potential scopes for policies:
Site collection policies, which can be bound to site-scoped content types. These are also referred to as "site policies." When you assign a site policy to a specific content type or list, a copy of the policy, known as a policy instance, is copied locally into the content type or list.
On first consideration, site collection policies sometimes seem preferable for enterprise-scope applications. However, these policies have limitations because they are not attached from the start to a content type, which is how they are ultimately manifested.
Content type policies at site or list scope. This is the preferred way of using policies. This scope is a function of how content type scoping works. Because content types can have site-wide scope, or be simple instances of a content type that is bound to a list, an attached policy follows the same scoping behavior as the content type. In other words, a policy attached to a site content type cannot be altered at the list or library level, and a policy attached to a list content type cannot be reused for other lists and libraries within the site. Site and List Content Types in the Windows SharePoint Services 3.0 SDK addresses this as follows:
"When you add a site content type to a list, Windows SharePoint Services 3.0 copies a local copy of the site content type onto the list itself. This local instance is called a list content type and applies only to the list onto which it was copied.
Because Windows SharePoint Services stores a copy of the site content type as a list content type on each list to which that site content type is added, you can make changes to a list content type without affecting the site content type itself. The changes to a list content type are limited to that list and do not affect the site content type, or any other content types that inherit from that same site content type."
List content types can have policies applied to just the list in question, if that content type does not already have a policy defined at the site scope level or the site collection scope level. Policies that are applied to content types at the site level or the site collection level cannot be changed at the list level. If you attempt to do so, an error message is returned that states you cannot change this policy because it is inherited from the site content type, and that to change it, you must change the policy at the parent (site) level. Because policies are designed to be applied from the top down and to apply to child content types, the system enables you to change other aspects of the content type, such as its name or its columns.
One big difference between site collection policies and content type policies is that when a site collection policy is created, there is no way of knowing what kind of content type the policy will be applied to. Consequently, only the fields or columns that are common across all the base or core content types can be exposed as properties that the policy can address. In the case of expiration policies, this means that site collection policies—at least those created via the UI—can reference only the Created Date and Last Modified Date when determining when to expire.
Throughout this article, expiration policies are used as an example, partly because my experiences in this area led to the writing of this article. Presumably, the other built-in SharePoint policies are mostly comparable.
Because site collection policies have the broadest possible scope, they are more commonly encountered at the enterprise level, where information architecture implicitly includes information life cycle and management. At present, however, because of limitations with site collection policies, you should first look to use site or list content policies and find ways to deploy them with the scope you want, by using feature stapling or an approach such as the one described in the following section.
Attaching Policy to Your Content Type
Usually, the first step in attaching policy to a content type is to create your own content type that you want the policy to apply to. Of course, you can use the content types that are built into SharePoint Server, but if the content type needs policy, you will most likely want your own type. More frequently, you will start with the built-in content types, and extend or alter them.
You can add policy to a content type through the UI or by using code, but you cannot add policy to a content type via the content type declaration in its defining Feature.
Attaching Policy to a Content Type Using Feature Activation
So, how do you get the intended information management policy attached to the content type? When features are installed, activated, deactivated, or uninstalled, SharePoint Server raises an event. In these events, you can perform actions that cannot be accomplished via the feature, or clean up residue that SharePoint Server cannot handle.
Adding a feature activation handler is the most convenient way to get policy attached to a content type. As you will see, it is essential to use code to perform certain other activities, such as modifying a sealed content type, or changing property values of pre-existing content. The feature activation handler is a great place to do this additional work.
Using a Feature Activation Handler
In the following example code, the interface requires that all four events have handlers of some kind, but three of the event handlers are just stubs.
The following code is simpler than what is shown in the accompanying sample code files, because it shows only the following basic actions:
Add a policy XML file into the site collection policies that are set.
Get the policy object.
Attach that policy to a content type with a known identifier.
This example assumes that either the content type feature was previously deployed, or that it is activated first via a separate feature that this content type feature depends upon. You could also create the content type dynamically by using the same technique used for the policy.
Making the policy XML a constant string within the code is a low-effort way to get started. You can also put it into a resource.
Note
Using the at sign (@) literal helps, but you must still enclose quotation marks within quotation marks (that is, surround double quotation marks with double quotation marks), and terminate the line with a semicolon (;).
using System; using System.Diagnostics; using Microsoft.SharePoint;
using Microsoft.Office.RecordsManagement.InformationPolicy;
namespace CTPolicyFeature
{ class CTPolicyReceiver : SPFeatureReceiver
{ static SPContentTypeId _ctid = new SPContentTypeId(
"0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237
AF39000893DEA125676F448E1412A2CEA82362" );
public readonly string _PolicyId =
"801fe194-1e51-4664-a1da-1c48f5c73ac1";
public readonly string _xmlPolicyDef =
@"<p:Policy xmlns:p=""office.server.policy"" local=""false""
id=""801fe194-1e51-4664-a1da-1c48f5c73ac1""> <p:Name>Dead
News</p:Name> <p:Description>News Flash expiration policy</p:Description>
<p:Statement>News flashes must expire within a day, and get deleted.</p:Statement>
<p:PolicyItems> <p:PolicyItem featureId=""Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration"">
<p:Name>Expiration</p:Name> <p:Description>Automatic scheduling
of content for processing, and expiry of content that has
reached its due date.</p:Description> <p:CustomData> <data> <formula
id=""Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn"">
<number>1</number>
<property>Modified</property>
<period>days</period>
</formula>
<action type=""action"" id=""Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.MoveToRecycleBin"" />
</data> </p:CustomData> </p:PolicyItem> </p:PolicyItems> </p:Policy>";
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPSite site = (SPSite)properties.Feature.Parent;
Policy pol = default(Policy); // Idiom for a reference object null value.
try
{
Policy.ValidateManifest(_xmlPolicyDef);
PolicyCollection.Add(site,_ xmlPolicyDef);
PolicyCatalog siteCatalog = new PolicyCatalog(site);
Pol = siteCatalog.PolicyList[ _ PolicyId ];
CtCreatePolicy(oTargetSite, _ctid, pol);
}
catch (SPException x)
{
logMessage(x.Message, EventLogEntryType.Error);
}
catch (Exception x)
{
logMessage(x.Message, EventLogEntryType.Error);
}
finally
{
site.Close();
if (pol != null)
{
pol.Dispose();
}
}
}
private void CtCreatePolicy( SPSite site, SPContentTypeId ctid, Policy plc )
{
SPContentTypeCollection ctc = site.RootWeb.ContentTypes;
SPContentType ctPage = ctc[ctid];
if ( Policy.GetPolicy(ctPage) != null )
{
logMessage( "Deleting Policy", EventLogEntryType.Information );
Policy.DeletePolicy(ctPage);
}
Policy.CreatePolicy(ctPage, plc);
// Set base type, and rely on push-down to work for child types.
ctPage.Update(true);
// This clears the "Dirty" bit and flushes the DirtyItemCollection.
Microsoft.Office.RecordsManagement.InformationPolicy.Policy.ProcessChanges(site);
logMessage("Policy created and added to content type", EventLogEntryType.Information);
}
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
}
public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
logMessage("Content Type Policy installed. Needs to be activated and applied.", EventLogEntryType.SuccessAudit);
}
public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
}
private void logMessage(string message, EventLogEntryType type)
{
if (!EventLog.SourceExists("SharePoint Features"))
{
EventLog.CreateEventSource("SharePoint Features", "Application");
}
EventLog.WriteEntry("SharePoint Features", message, type);
}
}
}
For easier testing, deploy an assembly built with this code into the global assembly cache during development. Your production scenario will depend on what else you are supporting—that is, whether you need to restrict scope to a Web application.
Making Changes Needed for Features
Because the preceding code references the content type by using an embedded GUID, the code depends on the content type with that GUID already being part of the target top-level site's content types, that is, it depends on the RootWeb property of the SPSite object.
Note
Site collections do not have a collection of content types, but the top-level site for the site collection does, and serves essentially the same purpose.
Although this works, it means you must ensure the following:
The content type feature is installed first.
The policy feature activation handler is a separate feature from the content type.
The policy feature is designed to depend on the content type feature.
The following example for Feature.xml corresponds to the second point. Notice it does not have any contained elements; assemblies are handled outside of the elements declaration. The activation dependency is essential if this feature is not deployed as part of a solution that guarantees activation order for features.
Feature.xml
<?xml version="1.0" encoding="utf-8" ?>
<Feature Scope="Site"
Title="Provision Fabrikam Policy To Site Collection"
Id="21E2DEB4-39A4-23c1-5E56-D1940DA7A089"
xmlns="https://schemas.microsoft.com/sharepoint/"
Hidden="FALSE"
ReceiverAssembly=" FabCTPolicy.dll, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=8ab849fcafe1db32"
ReceiverClass=" Fab.EventHandlers.CTPolicyFeature.CTPolicyReceiver ">
<ActivationDependencies>
<ActivationDependency FeatureId="68ba366d-cd49-495d-434a-185761ead54e" />
</ActivationDependencies>
</Feature>
Working with Sealed Content Types
Having a hierarchy of content types provides a great amount of functionality. However, the code is significantly complicated by a couple of content type attributes that are needed for security and to govern information architecture, and the information management you need generally to attach policy to a content type. In particular, these are the sealed and read-only attributes. The significant difference between these two is that the sealed attribute can be changed only by using the object model, whereas an administrator with the appropriate permissions can alter the read-only attribute through the UI.
From a systematic deployment perspective, the logic needed is equivalent, so we will just look at the sealed attribute.
In principle, before you can change or add policy to a sealed content type you must unseal the content type. Normally, changing a content type property enables you to push down the changes to all children or prevents you from doing so. However, trying to change the sealed property at the top-level of a content type hierarchy FAILS when it attempts to change the child content type's sealed property because the child is still sealed.
The remedy is to start at the leaf nodes of the content type hierarchy, unseal these, and then move back to the immediate parent, visiting all the leaf nodes in reverse order. Recursion to the rescue. The algorithm that accounts for all extant content types in a given site, including those list instance content types, looks like the following code (the first call to this would normally be at the top-level site of the site).
private void AlterSealFabTypes(SPWeb web, SPContentTypeId []
ctIds, bool sValue )
{ // Termination test for recursing to the leaves.
if (web.Webs != null)
{ foreach (SPWeb wb in web.Webs)
{ try
{ AlterSealFabTypes(wb, ctIds, sValue); }
finally
{ wb.Close(); }} }
// First the list instances.
SPList pages = null;
SPListTemplateType pt = (SPListTemplateType)850;
foreach (SPList l in web.Lists)
{ UpdateSeal(l.ContentTypes, ctIds, sValue);
}
// Then the Web itself.
if (web.ContentTypes != null && web.ContentTypes.Count > 0)
{ UpdateSeal(web.ContentTypes, ctIds, sValue);
} }
private void UpdateSeal( SPContentTypeCollection ctc,
SPContentTypeId [] alterCtids, bool sealingValue )
{ foreach (SPContentType ct in ctc)
{ if( TestForFabContentTypes( ct.Id, alterCtids ) )
{
ct.Sealed = sealingValue;
ct.Update( false );
} } }
private bool TestForFabContentTypes(SPContentTypeId testId,
SPContentTypeId[] FabCtTypes)
{
foreach (SPContentTypeId FabCtid in FabCtTypes)
{ if( FabCtid == testId ||
FabCtid.IsParentOf( testId ) ||
testId.IsChildOf( FabCtid ) )
{ return true;
} }
return false;
}
Modifying Existing Content and Content Types, and Provisioning for Future Sites
Rarely do you have the luxury of starting with a blank slate when it comes to introducing content types and adding information management policy to these types at inception. Even given this opportunity, and given that most deployments occur in stages and phases, you will probably encounter the need to modify already deployed content types, and handle extant content that is based on those types.
In addition, new sites and even new Web applications are part of the evolution of any significant SharePoint Server deployment.
So, the process of adding policy must account for all of these situations.
Activation Scope: Web Application or Site Collection
When the feature activation handler runs, it can detect whether it is being run in the scope of just a site (site collection), or whether it is being run for an entire Web application. The presumption for a Web application is that all site collections within the Web application should be visited and the feature activated.
Conversely, in principle, the same idea applies to deactivation. But removing policy and potentially removing other elements from a content type is well beyond the scope of this project, and perhaps impossible. Refer to the Windows SharePoint Services SDK and to Microsoft TechNet for things you can do with a solution, and changes you can make to already extant content types, and the effect of removing content types from extant and deployed SharePoint Server instances.
The sample code (Information Management Policy Deployment Sample for SharePoint Server 2007) that accompanies this article handles activation as either a Web application or a site, but a separate feature is probably necessary to support Web application scope activation. The code sample does not provide this.
Provisioning with Feature Stapling
Sites you create in the future that are meant to contain the content type and policies, such as those in this sample, can be provisioned for by "stapling" the feature activation handler and the content type features to a given site template. Feature Stapling is also accomplished via a feature. Because feature stapling is well documented elsewhere (for example, see Feature Stapling in the Microsoft Office SharePoint Server 2007 SDK), a sample for this purpose is not provided here.
Applying Policy to Instantiated Site and List Content Types
If you truly could start with a blank slate, it would be sufficient to just modify the target content type with policy at the site scope. This would assume that no lists were created that made use of this content type, because after they are created, a list-scoped content type instance is also created. Applying policy to the site content type after this happens does not immediately cause the instanced content types to be updated. Instead, this potentially expensive operation is deferred until a system timer job runs. By default, the timer job runs once a day in the middle of the night. However, you can set it to run more frequently by using the setpolicyschedule command in Stsadm.exe.
Workaround for FieldCollection Update Failure
There is one issue that you might encounter as you try to use this article and the accompanying sample code. It concerns adding custom fields to child content types. Specifically, after adding policy to the base content type, and resealing the base content type, custom fields that are added by the content type definition in the feature are not recreated in the child content types.
The Windows SharePoint Services 3.0 SDK states:
"The SPFieldCollection object provides developers a way to get a 'merged view' of a column's attributes, as they are in that content type. Each SPField object represents all the attributes of a column definition, or field definition, merged with those attributes that have been overridden in the field reference for that content type.
When you access an SPField in a content type, Windows SharePoint Services merges the field definition with the field reference, and returns the resulting SPField object to you. This prevents developers from having to look up a field definition, and then look up all the attributes in the field definition overridden by the field reference for that content type.
Because of this, there is a 1-to-1 correlation between the items in the SPFieldLinkCollection and SPFieldCollection objects. For each SPFieldLink you add to a content type, Windows SharePoint Services adds a corresponding SPField object that represents the merged view of that column as it's defined in the content type.
You cannot directly add or delete items from an SPFieldCollection object in an SPContentType object; trying to do so throws an error."
In other words, everything in the SPFieldLinkCollection SHOULD show up in the SPFieldCollection, but it might not, and did not when this article was written.
The workaround shown in the sample code captures the Field Reference before performing the content type update, and then reapplies it to the child content types by adding and deleting the Field Reference. For this workaround to be effective, it must be safe to lose the original information. For the sample, this is indeed the case because all of these fields are affected by the policy.
Conclusion
This article describes how to deploy policies operationally. The essential lesson behind this article is that extending content types with behavior is an extremely valuable addition from SharePoint Server 2007 to the underlying Windows SharePoint Services 3.0 model. The ability to manage individual instances of the type by using policy is worth the development effort required to deploy it. You can avoid much of the deployment complexity by planning in advance, and by not sealing the content type, at least until development is complete.
Additional Resources
For more information, see the following resources: