Create Document Retention MOSS 2007 – Part I
I know I haven't written for quite some time and I am apologetic for that, however, this post hopefully will make up for it.
How many times have you said to yourself, I need a document retention policy inside of SharePoint 2007 regardless of what document libraries we create or documents we upload? Furthermore, I want the ability to "pause" or put on some sort of "legal hold" on a document to push out the archiving and disposal of these documents. Finally, I have quite a few content types and would like to potentially treat these with different retention policies and they may not go to any record center because we cannot ensure they are records. I have had numerous customers who have asked for this and my recent answer is oh, wait until SharePoint 2010 which they frown and say oh.
In this post, I am going to provide to you a way to turn the oh, into Ah HA!
We are going to write a series of features that will provide all of the above functionality.
Step 1:
In most solution packages you will have a fields feature and that will be simply provisioning the fields you need. In this case, we are going to write the feature.xml file like:
<?xml version="1.0" encoding="utf-8" ?> <Feature Id="BF67889E-FBDF-40d5-A6A6-B6F9C9CE6658" Title="MyFields" Description="This feature provisions all fields to necessitate appropriate metadata" Version="12.0.0.0" Scope="Site" xmlns="https://schemas.microsoft.com/sharepoint/"> <ElementManifests> <ElementManifest Location="fields.xml"/> </ElementManifests> </Feature> |
And the fields.xml feature like:
<?xml version="1.0" encoding="utf-8" ?> <Elements xmlns="https://schemas.microsoft.com/sharepoint/"> <Field ID="{84027183-0549-4bfb-8601-7DDDB12EED12}" Name="LegalHold" SourceID="https://schemas.microsoft.com/sharepoint/v3" StaticName="LegalHold" Group="ABC" Type="Boolean" Description="Allows individual to place document on hold" DisplayName="LegalHold"> <Default>0</Default> </Field> <Field ID="{DBA5FD1B-97A0-470f-89B8-EB98BC28061A}" Name="LegalStartDate" SourceID="https://schemas.microsoft.com/sharepoint/v3" StaticName="LegalStartDate" Group="ABC" Type="DateTime" Hidden="FALSE" ReadOnly="TRUE" Description="Tells record when the start of the last legal hold was" DisplayName="Legal Start Date" /> <Field ID="{14CE4BF3-F83A-410d-A454-A12FD20A60A9}" Name="LegalEndDate" SourceID="https://schemas.microsoft.com/sharepoint/v3" StaticName="LegalEndDate" Group="ABC" Type="DateTime" Hidden="FALSE" ReadOnly="TRUE" Description="Tells record when the end of the last legal hold was" DisplayName="Legal End Date" /> <Field ID="{DBA90AA4-B671-4c49-9ABF-C8C819DE0F13}" Name="ArchiveDate" SourceID="https://schemas.microsoft.com/sharepoint/v3" StaticName="ArchiveDate" Group="ABC" Type="DateTime" Hidden="FALSE" ReadOnly="TRUE" Description="Tells retention when to archive record" DisplayName="Archive Date" /> <Field ID="{1D6D2CE5-ABCD-4368-829B-085D3FBE99FB}" Name="DisposeDate" SourceID="https://schemas.microsoft.com/sharepoint/v3" StaticName="DisposeDate" Group="ABC" Type="DateTime" Hidden="FALSE" ReadOnly="TRUE" Description="Tells retention when to delete the record" DisplayName="Dispose Date" /> <Field ID="{1CDE08F2-DAF6-4449-B481-31095207A983}" Name="NewStartDate" SourceID="https://schemas.microsoft.com/sharepoint/v3" StaticName="NewStartDate" Group="ABC" Type="DateTime" Hidden="FALSE" ReadOnly="TRUE" Description="This field is used to keep track of new retention schedule based on Legal Holds" DisplayName="New Start Date" /> <Field ID="{96AAF204-A5E0-4851-A6F4-B3235EBEA185}" Name="_Archival" SourceID="https://schemas.microsoft.com/sharepoint/v3" StaticName="_Archival" Group="ABC" Type="Integer" Description="(in years) how long before record get archived" DisplayName="Archive Schedule" /> <Field ID="{67A14E20-617B-46d8-8598-433782CC9E4E}" Name="_Disposal" SourceID="https://schemas.microsoft.com/sharepoint/v3" StaticName="_Disposal" Group="ABC" Type="Integer" Description="(in years) how long before record get deleted" DisplayName="Dispose Date" /> <Field ID="{2A8BB895-2D13-4300-BF67-487A82BA00AD}" Name="_Archived" SourceID="https://schemas.microsoft.com/sharepoint/v3" StaticName="_Archived" Group="ABC" Type="Boolean" Hidden="FALSE" ReadOnly="TRUE" Description="Is record in archive status" DisplayName="Archived"> <Default>0</Default> </Field> </Elements> |
Step 2:
The first feature is going to be scoped at a "Site" level and will extend the document library content type by adding the necessary fields needed to make this work.
I will call this feature LegalHold and I will simply point to a feature activation receiver.
The feature.xml file will look something like:
<?xml version="1.0" encoding="utf-8" ?> <Feature Id="38B5399F-4DAB-4938-8F92-B474A319F9B2" Title="Legal Holds" Description="This feature provides an extension to the document content type to allow for legal holds" Version="12.0.0.0" Scope="Site" ReceiverAssembly="MyNameSpace.Features, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f6e07d4bee530c01" ReceiverClass="MyNameSpace.Features.LegalHoldFeatureReceiver" xmlns="https://schemas.microsoft.com/sharepoint/" > </Feature > |
The receiver code will look something like this:
using System; using Microsoft.SharePoint; namespace MyNameSpace.Features { class LegalHoldFeatureReceiver : SPFeatureReceiver {
public static void LinkFieldToContentType(SPWeb web,string contentType, SPField field) { SPContentType ct = web.ContentTypes[contentType]; ct.FieldLinks.Add(new SPFieldLink(field)); ct.Update(); }
public override void FeatureActivated(SPFeatureReceiverProperties properties) { //Expand Document Content Type to add additional Fields and Disposition Workflow using (SPSite spsite = (SPSite )properties.Feature.Parent) { using (SPWeb spweb = spsite.RootWeb) { try { //Extend Document Content Type
LinkFieldToContentType(spweb, "Document", spweb.Fields[new Guid("{84027183-0549-4bfb-8601-7DDDB12EED12}")]);
LinkFieldToContentType(spweb, "Document", spweb.Fields[new Guid("{DBA5FD1B-97A0-470f-89B8-EB98BC28061A}")]);
LinkFieldToContentType(spweb, "Document", spweb.Fields[new Guid("{14CE4BF3-F83A-410d-A454-A12FD20A60A9}")]);
LinkFieldToContentType(spweb, "Document", spweb.Fields[new Guid("{DBA90AA4-B671-4c49-9ABF-C8C819DE0F13}")]);
LinkFieldToContentType(spweb, "Document", spweb.Fields[new Guid("{1D6D2CE5-ABCD-4368-829B-085D3FBE99FB}")]);
LinkFieldToContentType(spweb, "Document", spweb.Fields[new Guid("{1CDE08F2-DAF6-4449-B481-31095207A983}")]);
LinkFieldToContentType(spweb, "Document", spweb.Fields[new Guid("{2A8BB895-2D13-4300-BF67-487A82BA00AD}")]); } catch { } } } } public override void FeatureDeactivating(SPFeatureReceiverProperties properties) { throw new NotImplementedException (); } public override void FeatureInstalled(SPFeatureReceiverProperties properties) { throw new NotImplementedException (); } public override void FeatureUninstalling(SPFeatureReceiverProperties properties) { throw new NotImplementedException (); } } } |
Step 3:
We then are going to write the feature to "staple" the former feature to the farm globally. You can choose to only staple site definitions but in most cases I have found that the customer wants this "everywhere".
We are going to name this feature "MyKewlStaple"
The feature.xml file will simply point to the elements file as such:
<?xml version="1.0" encoding="utf-8" ?> <Feature Id="ACC0BA0A-E0AF-4ee0-BECB-94B8C71E2B18" Title="My Kewl Staple" Description="This feature staples all other features to appropriate site templates" Version="12.0.0.0" Scope="Farm" xmlns="https://schemas.microsoft.com/sharepoint/"> <ElementManifests> <ElementManifest Location="elements.xml"/> </ElementManifests> </Feature> |
The elements.xml file will do the staple like so:
<Elements xmlns="https://schemas.microsoft.com/sharepoint/"> <!--Global Features--> <!--Legal Holds--> <FeatureSiteTemplateAssociation Id="38B5399F-4DAB-4938-8F92-B474A319F9B2" TemplateName="GLOBAL" /> <FeatureSiteTemplateAssociation Id="A13B775C-FB57-4171-9907-D2B97A3F9B6F" TemplateName="GLOBAL" /> </Elements> |
Step 4:
Now we want to be able to give the users somewhere to provision a retention schedule list in which they can modify retention schedules by content type. In my scenario I have an administration site under "sites/admin" and so we will be provisioning the list there. Because I do everything through content types, I will also create the content type feature in this step.
The feature.xml for the content type will be:
<?xml version="1.0" encoding="utf-8" ?> <Feature Id="F45B1769-EBF2-4bc9-A0DA-F89B6802DB95" Title="ABC Content Types" Description="This feature provisions all Content Types to necessitate appropriate metadata" Version="12.0.0.0" Scope="Site" xmlns="https://schemas.microsoft.com/sharepoint/"> <ElementManifests> <ElementManifest Location="ctypes.xml"/> </ElementManifests> </Feature> |
The ctypes.xml which provisions the content type will be:
<Elements xmlns="https://schemas.microsoft.com/sharepoint/"> <ContentType ID="0x01A4" BaseType="0x01" Description="This stores Retention Schedule by Content Type" Group="ABC" Name="RetentionList" Version="0"> <FieldRefs> <FieldRef ID="{96AAF204-A5E0-4851-A6F4-B3235EBEA185}" Name="_Archival" DisplayName="Archive Schedule" /> <FieldRef ID="{67A14E20-617B-46d8-8598-433782CC9E4E}" Name="_Disposal" DisplayName="Dispose Schedule" /> </FieldRefs> |
The feature.xml for the retention list looks like:
<?xml version="1.0" encoding="utf-8" ?> <Feature Id="A4CC0C25-6697-42cf-9ED4-94ED548B9186" Title="Retention List" Description="This feature provisions the Content Type Retention List" Version="12.0.0.0" Scope="Web" xmlns="https://schemas.microsoft.com/sharepoint/"> <ElementManifests> <ElementManifest Location="Retention\ListTemplateElements.xml"/> <ElementManifest Location="Retention\ListInstanceElements.xml"/> <ElementFile Location="Retention\schema.xml"/> </ElementManifests> </Feature> |
I provide the template for the retention list which looks like (ListTemplateElements):
<?xml version="1.0" encoding="utf-8" ?> <Elements xmlns="https://schemas.microsoft.com/sharepoint/"> <ListTemplate Name="Retention" DisplayName="Retention" Description="This template provides the retention schedules by content type" BaseType="0" Type ="6003" OnQuickLaunch="TRUE" SecurityBits="11" Sequence="410" Image="/_layouts/images/itgen.gif" /> </Elements> |
I instantiate the list like (ListInstanceElements):
<?xml version="1.0" encoding="utf-8" ?> <Elements xmlns="https://schemas.microsoft.com/sharepoint/"> <ListInstance Id="6537B4F6-13B7-4d48-BC90-D01483859A95" Title="Retention Schedule" Url="Lists/Retention" Description="This list the retention schedule for ABC" TemplateType="6003" OnQuickLaunch="TRUE"> <Data> <Rows> <Row> <Field Name="Title">Default</Field> <Field Name="_Archival">6</Field> <Field Name="_Disposal">1</Field> </Row> </Rows> </Data> </ListInstance> </Elements> |
Finally, I will provide just the pieces of the schema.xml file that I have changed. For a shortcut, I first copy the schema.xml file from 12\templates\features\customlist\custlist\schema.xml.
I changed the following sections:
First line:
<List xmlns:ows="Microsoft SharePoint" Title="Retention Schedule" FolderCreation="FALSE" Direction="$Resources:Direction;" Url="Lists/Retention" BaseType="0">
<MetaData> <ContentTypes> <ContentTypeRef ID="0x01A4"> <Folder TargetName="Item" /> </ContentTypeRef> <ContentTypeRef ID="0x0120" /> </ContentTypes> <Fields> <Field ID="{96AAF204-A5E0-4851-A6F4-B3235EBEA185}" Name="_Archival" Type="Integer" Description="(in years) how long before record get archived" DisplayName="Archive Schedule" /> <Field ID="{67A14E20-617B-46d8-8598-433782CC9E4E}" Name="_Disposal" Type="Integer" Description="(in years) how long before record get deleted" DisplayName="Dispose Date" /> </Fields> |
ViewFields:
<ViewFields> <FieldRef Name="Attachments" /> <FieldRef Name="LinkTitle" /> <FieldRef Name="_Archival" /> <FieldRef Name="_Disposal" /> </ViewFields> |
Step :
Now that we have a place that the users can modify retention schedules, we have provisioned the fields, added them to content types, and extended the document content type we are ready for the "final" step which will be creating the event receivers that are attached to every list template id of 101 which will be all document libraries created in the farm.
The feature.xml files will look like:
<?xml version="1.0" encoding="utf-8" ?> <Feature Id="A13B775C-FB57-4171-9907-D2B97A3F9B6F" Title="Legal Holds" Description="This feature provides document item event receiver for legal holds" Version="12.0.0.0" Scope="Web" xmlns="https://schemas.microsoft.com/sharepoint/"> <ElementManifests> <ElementManifest Location="elements.xml"/> </ElementManifests> </Feature> |
The interesting thing about the above is that it will go ahead and attach the receivers to all document library templates.
The elements file for attaching the code would look like:
<?xml version="1.0" encoding="utf-8" ?> <Elements xmlns="https://schemas.microsoft.com/sharepoint/"> <Receivers ListTemplateId="101"> <Receiver> <Name>Document Adding Event</Name> <Type>ItemAdded</Type> <SequenceNumber>25000</SequenceNumber> <Assembly>MyNameSpace.Features, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f6e07d4bee530c01</Assembly> <Class>MyNameSpace.Features.LegalHoldItemEventReceiver</Class> <Data></Data> <Filter></Filter> </Receiver> </Receivers> <Receivers ListTemplateId="101"> <Receiver> <Name>Document Updated Event</Name> <Type>ItemUpdated</Type> <SequenceNumber>25001</SequenceNumber> <Assembly>MyNameSpace.Features, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f6e07d4bee530c01</Assembly> <Class>MyNameSpace.Features.LegalHoldItemEventReceiver</Class> <Data></Data> <Filter></Filter> </Receiver> </Receivers> </Elements> |
The code for the receiver would look like:
using System; using Microsoft.SharePoint; namespace MyNameSpace.Features { class LegalHoldItemEventReceiver : SPItemEventReceiver { //Sets public override void ItemAdded(SPItemEventProperties properties) { base.ItemAdded(properties); using (SPWeb spweb = properties.OpenWeb()) { if (spweb.Fields.ContainsField("NewStartDate")) { //Set the base counter for archive properties.ListItem["NewStartDate"] = DateTime.Today;
//Get list of content types and set retention schedule string AdminUrl = spweb.Url.Replace(spweb.ServerRelativeUrl,string.Empty) + @"/sites/admin"; string contentType = properties.ListItem.ContentType.Name;
//Set Default Values if no default set int archiveSchedule = 6; int disposeSchedule = 1; using (SPSite adminSite = new SPSite(AdminUrl)) { using (SPWeb adminWeb = adminSite.OpenWeb()) { SPList lstRetention = adminWeb.Lists["Retention Schedule"]; //Run CAML for Content Type string strQuery = @"<Where><Eq><FieldRef Name='Title' /><Value Type='Text'>" + contentType + @"</Value></Eq></Where>"; SPQuery ctQuery = new SPQuery(); ctQuery.ViewFields = "<FieldRef Name='Title'/><FieldRef Name='_Archival'/><FieldRef Name='_Disposal'/>"; ctQuery.Query = strQuery; SPListItemCollection splic = lstRetention.GetItems(ctQuery); if (splic.Count > 0) { archiveSchedule = (int)splic[0][new Guid("{96AAF204-A5E0-4851-A6F4-B3235EBEA185}")]; disposeSchedule = (int)splic[0][new Guid("{67A14E20-617B-46d8-8598-433782CC9E4E}")]; } else { //if no results Run CAML for Default strQuery = @"<Where><Eq><FieldRef Name='Title' /><Value Type='Text'>Default</Value></Eq></Where>"; ctQuery = new SPQuery(); ctQuery.ViewFields = "<FieldRef Name='Title'/><FieldRef Name='_Archival'/><FieldRef Name='_Disposal'/>"; ctQuery.Query = strQuery; splic = lstRetention.GetItems(ctQuery); if (splic.Count > 0) { archiveSchedule = (int)splic[0][new Guid("{96AAF204-A5E0-4851-A6F4-B3235EBEA185}")]; disposeSchedule = (int)splic[0][new Guid("{67A14E20-617B-46d8-8598-433782CC9E4E}")]; } } } }
//Set Archive and Dispose Dates for record properties.ListItem["ArchiveDate"] = DateTime.Today.AddYears(archiveSchedule); properties.ListItem["DisposeDate"] = DateTime.Today.AddYears(archiveSchedule + disposeSchedule); this.DisableEventFiring(); try { properties.ListItem.Update(); } catch { } finally { this.EnableEventFiring(); } } } } public override void ItemUpdated(SPItemEventProperties properties) { base.ItemUpdated(properties); using (SPWeb spweb = properties.OpenWeb()) { //Only run the code if the Legal Hold box has been modified if (spweb.Fields.ContainsField("LegalHold")) { bool BeforeLegalHold = true; bool AfterLegalHold = true; if (properties.BeforeProperties["LegalHold"] == null || properties.BeforeProperties["LegalHold"].ToString() == "false") { BeforeLegalHold = false; } if (properties.AfterProperties["LegalHold"] == null || properties.AfterProperties["LegalHold"].ToString() == "false") { AfterLegalHold = false; } if (BeforeLegalHold != AfterLegalHold) { //Get list of content types and set retention schedule string AdminUrl = spweb.Url.Replace(spweb.ServerRelativeUrl, string.Empty) + @"/sites/admin"; string contentType = properties.ListItem.ContentType.Name; //Set Default Values if no default set int archiveSchedule = 6; int disposeSchedule = 1; using (SPSite adminSite = new SPSite(AdminUrl)) { using (SPWeb adminWeb = adminSite.OpenWeb()) { SPList lstRetention = adminWeb.Lists["Retention Schedule"]; //Run CAML for Content Type string strQuery = @"<Where><Eq><FieldRef Name='Title' /><Value Type='Text'>" + contentType + @"</Value></Eq></Where>"; SPQuery ctQuery = new SPQuery(); ctQuery.ViewFields = "<FieldRef Name='Title'/><FieldRef Name='_Archival'/><FieldRef Name='_Disposal'/>"; ctQuery.Query = strQuery; SPListItemCollection splic = lstRetention.GetItems(ctQuery); if (splic.Count > 0) { archiveSchedule = (int)splic[0][new Guid("{96AAF204-A5E0-4851-A6F4-B3235EBEA185}")]; disposeSchedule = (int)splic[0][new Guid("{67A14E20-617B-46d8-8598-433782CC9E4E}")]; } else { //if no results Run CAML for Default strQuery = @"<Where><Eq><FieldRef Name='Title' /><Value Type='Text'>Default</Value></Eq></Where>"; ctQuery = new SPQuery(); ctQuery.ViewFields = "<FieldRef Name='Title'/><FieldRef Name='_Archival'/><FieldRef Name='_Disposal'/>"; ctQuery.Query = strQuery; splic = lstRetention.GetItems(ctQuery); if (splic.Count > 0) { archiveSchedule = (int)splic[0][new Guid("{96AAF204-A5E0-4851-A6F4-B3235EBEA185}")]; disposeSchedule = (int)splic[0][new Guid("{67A14E20-617B-46d8-8598-433782CC9E4E}")]; } } } } if (AfterLegalHold) { //Empty Archive, Dispose and Last Legal End Dates properties.ListItem[new Guid("{DBA90AA4-B671-4c49-9ABF-C8C819DE0F13}")] = null; properties.ListItem[new Guid("{1D6D2CE5-ABCD-4368-829B-085D3FBE99FB}")] = null; properties.ListItem[new Guid("{14CE4BF3-F83A-410d-A454-A12FD20A60A9}")] = null; //Set Last Legal Start Date properties.ListItem[new Guid("{DBA5FD1B-97A0-470f-89B8-EB98BC28061A}")] = DateTime.Today; } else { //Set Last Legal End Dates, Base Counter properties.ListItem[new Guid("{14CE4BF3-F83A-410d-A454-A12FD20A60A9}")] = DateTime.Today; //Get Difference between start and end legal hold dates TimeSpan datediff = DateTime.Today.Subtract((DateTime)properties.ListItem[new Guid("{DBA5FD1B-97A0-470f-89B8-EB98BC28061A}")]); //Get current base counter date DateTime dtCurrentBaseCounterDate = (DateTime)properties.ListItem[new Guid("{1CDE08F2-DAF6-4449-B481- 31095207A983}")]; properties.ListItem[new Guid("{1CDE08F2-DAF6-4449-B481-31095207A983}")] = dtCurrentBaseCounterDate.AddDays(datediff.Days); //Set Archive, Dispose Dates based on retention schedules properties.ListItem[new Guid("{DBA90AA4-B671-4c49-9ABF-C8C819DE0F13}")] = dtCurrentBaseCounterDate.AddDays(datediff.Days).AddYears(archiveSchedule); properties.ListItem[new Guid("{1D6D2CE5-ABCD-4368-829B-085D3FBE99FB}")] = dtCurrentBaseCounterDate.AddDays(datediff.Days).AddYears(archiveSchedule + disposeSchedule); } this.DisableEventFiring(); try { properties.ListItem.Update(); } catch { } finally { this.EnableEventFiring(); } } } } } } } |
After all this, you now have an automatically calculated Archive and Dispose date that the users can control via content type as well as the ability to pause based on legal holds!
In the next post on this series, I will provide the mechanism to automatically provision the scope that will tell what documents need to be archived and deleted as well as provide the timer job that does so!
Hope this helps