22 October 2009

End-To-End Solution – Custom Document Library and Completed Feature Activated

Life has been interesting the past week or so. I have a co-worker that will be out for a few weeks and have been busy getting some things finished prior to his departure. During this time, I have taken on some additional responsibilities that have slowed my progress a bit. However, I’m still planning on finishing this series. The goal is to complete my feature this week and dive into using some of the SharePoint web services in an ASP.NET application.

In this post, I plan to cover the development of a custom document library to hold all of my KB articles. This custom document library is being used so I can create a custom action and application page to activate and deactivate an event receiver specific to this document library.

Custom Document Library Definition

In the last post, I described how I created a custom list for use in hosting the available categories for my KB articles. Using the same techniques, we will add a new custom document library template that will hold the KB articles for use in the final solution. Because I need block attempts to delete KB articles, I will need an event receiver to fire each time an attempt is made to remove an item from the library. However, I need to have the ability for site administrators to selectively turn this on and off as there may be times when an article needs to be removed.

The first attempt was to use a standard document library and attach the custom action to the library, but it can be difficult to find the proper library as you really need to know the GUID of the list. By creating a custom document library, the custom action can be bound to a specific type of list.

Here is the definition of the custom document library:

  1:   <ListTemplate Name="kbdoclib"
  2:                 Type="10801"
  3:                 BaseType="1"
  4:                 OnQuickLaunch="TRUE"
  5:                 SecurityBits="11"
  6:                 Sequence="1000"
  7:                 DisplayName="KB Document Library"
  8:                 Hidden="TRUE"
  9:                 Description="Document library containing documents for use in authoring KB Articles."
 10:                 Image="/_layouts/images/itdl.gif"
 11:                 Category="Libraries"/>

For the Type attribute – line 2 above – the next available integer above 10000 was chosen based on our organization’s requirements. Since the category list template type was 10800, the next available number for our organization is 10801.

In the category list template, the BaseType was set to 0 indicating that the template would be derived from the generic list. This time, the BaseType attribute for the list is 1. This indicates that this list template will be a document library. For those that are interested, here are the base types:

  1. BaseType=”0” - Generic List
  2. BaseType=”1” – Document Library
  3. BaseType=”3” – Discussion Forum
  4. BaseType=”4” – Vote or Survey
  5. BaseType=”5” – Issues List

Please notice there is no BaseType of 2. Todd Bleeker points out in his book Developer’s Guide to Windows SharePoint Services 3.0 that this was not implemented in WSS v2 or in the current version.

This XML fragment is part of the file named CustomListDefinitions.xml located in the root of the solution. Here is the entire contents of the file:

  1: <?xml version="1.0" encoding="utf-8" ?>
  2: <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3:   <ListTemplate Type="10800"
  4:                 BaseType="0"
  5:                 Name="CategoryList"
  6:                 DisplayName="Category List"
  7:                 Description="Contains a list of categories"
  8:                 SecurityBits="11"
  9:                 VersioningEnabled="false"
 10:                 Hidden="TRUE"
 11:                 DisableAttachments="true"
 12:                 Category="Custom Lists"
 13:                 Sequence="100"/>
 14:   <ListTemplate Name="kbdoclib"
 15:                 Type="10801"
 16:                 BaseType="1"
 17:                 OnQuickLaunch="TRUE"
 18:                 SecurityBits="11"
 19:                 Sequence="1000"
 20:                 DisplayName="KB Document Library"
 21:                 Hidden="TRUE"
 22:                 Description="Document library containing documents for use in authoring KB Articles."
 23:                 Image="/_layouts/images/itdl.gif"
 24:                 Category="Libraries"/>
 25: </Elements>

Now a schema.xml file is needed. This file should be placed in a folder named kbdoclib under the root of the feature’s folder. To simplify this process, the default document library schema is copied from <12 hive>/TEMPLATES/FEATURES/DocLib/DocLib/schema.xml. Nothing will be changed in the schema of the template. Instead, it has been elected that the feature activated event will do the heavy lifting of adding the required content types and assigning new views.

Completing Feature Activated

Now, I’m going to post a lot of the code that is used by the feature activated and then explain how it all works in this solution.

First up is the FeatureActivated method:

  1:     Public Overrides Sub FeatureActivated(ByVal properties As Microsoft.SharePoint.SPFeatureReceiverProperties)
  2:         Using mSite As SPSite = CType(properties.Feature.Parent, SPSite)
  3:             Using mWeb As SPWeb = mSite.RootWeb
  4:                 CreateApproverRole(mWeb)
  5:                 CreateApproverGroup(mWeb)
  6:                 Dim list As SPList = CreateCategoryListInstance(mWeb)
  7:                 CreateCategoryLookupSiteColumn(mWeb, list)
  8:                 UpdateKBDocumentContentType(mWeb)
  9:                 Dim library As SPDocumentLibrary = CreateKBDocumentLibrary(mWeb)
 10:             End Using
 11:         End Using
 12:     End Sub

To help with readability, I have broken the code down into the steps I will be taking when this feature is activated.

  1. First, I need to get a reference to the current site and web (lines 2 and 3).
  2. Next, I will be creating a new role within SharePoint that will be used to handle approval of documents in the KB document library (line 4).
  3. The role will be assigned to a group. (line 5)
  4. A new category list will be created based on the list definition defined in a previous article. (line 6)
  5. A new site column will be created that will lookup information stored in the category list. (line 7)
  6. The KB Content type will be updated to use the new site column created in step 5. (line 8)
  7. A new document library will be created utilizing all of the features that have been provisioned.

Method: CreateApproverRole

  1:     Protected Sub CreateApproverRole(ByVal web As SPWeb)
  2:         ' Create a new role
  3:         Dim ApproveRole As SPRoleDefinition
  4:         Dim createRole As Boolean = False
  5:         Try
  6:             ApproveRole = web.RoleDefinitions("Approve")
  7:         Catch ex As Exception
  8:             createRole = True
  9:         End Try
 11:         If createRole = True Then
 12:             ApproveRole = New SPRoleDefinition()
 14:             ' Get the contribute role as a base
 15:             Dim ContributeRole As SPRoleDefinition = web.RoleDefinitions("Contribute")
 17:             ' Add the approval rights to the role and set name and description
 18:             ApproveRole.BasePermissions = ContributeRole.BasePermissions Or SPBasePermissions.ApproveItems
 19:             ApproveRole.Description = "Allows add, edit, delete and approve rights to list items in a site."
 20:             ApproveRole.Name = "Approve"
 22:             ' Add the new role to the web
 23:             web.RoleDefinitions.Add(ApproveRole)
 25:             ' Update the web
 26:             web.Update()
 27:         End If
 28:     End Sub

So that a duplicate role is not created, an attempt to retrieve an existing role named “Approve” is made.

If the role does not exist, then an exception will be thrown that will indicate the role could not be found on the current site. If the role must be created, the Contribute role is used as the base (line 15). The role has approve rights added and a description and name set. This is then added to the SPWeb and the all important SPWeb.Update() method is called to apply these changes (line 26).

These role definitions are the permission settings that can be found in Permission Levels under Advanced Permissions > Permission Levels.


Method: CreateApproverGroup

Now that the role has been created, a new group will need to be added so users can be added/removed from this role easily.

  1:     Protected Sub CreateApproverGroup(ByVal web As SPWeb)
  2:         ' Get the owner group by finding the group with full permissions on the site.
  3:         Dim OwnerGroup As SPGroup = Nothing
  4:         OwnerGroup = web.AssociatedOwnerGroup 'web.ParentWeb.AssociatedOwnerGroup
  6:         Dim Approvers As SPGroup
  7:         Dim createGroup As Boolean = False
  8:         Try
  9:             Approvers = web.SiteGroups("Approvers")
 10:         Catch ex As Exception
 11:             createGroup = True
 12:         End Try
 14:         ' Create the approvers group
 15:         If createGroup = True Then
 16:             web.SiteGroups.Add("Approvers", OwnerGroup, web.CurrentUser, "Allows users to approve items in lists and libraries.")
 17:             Dim ApproverGroup As SPGroup = web.SiteGroups("Approvers")
 19:             ' Set the permission via a role definition for the group.
 20:             Dim ApproverGroupAssignment As New SPRoleAssignment(ApproverGroup)
 21:             Dim ApproverGroupRoleDefinition As SPRoleDefinition = web.RoleDefinitions("Approve")
 22:             ApproverGroupAssignment.RoleDefinitionBindings.Add(ApproverGroupRoleDefinition)
 23:             web.RoleAssignments.Add(ApproverGroupAssignment)
 25:             ' Apply the updates to the web site
 26:             web.Update()
 27:         End If
 28:     End Sub

Again, similar logic will be used to determine if the group needs to be added to this web site. One thing to point out, I struggled with how to get the current owner group since I determined I would prefer the owner of this new group to be the Site Owners group instead of the user activating the feature. After searching the MSDN documentation, I happened across a little property that I had not seen before on the SPWeb object – AssociatedOwnerGroup (line 4). Using this, the owner of the new group could easily be set to the owner group of the site.

If the group is not found, it is added to the site via the SPWeb object (line 16). After it is added, the Approver role needs to be assigned to the group.

  1. To do this the group is retrieved from the SiteGroups property of the SPWeb. (line 17)
  2. A new role assignment is created. (line 20)
  3. The role definition is retrieved. (line 21)
  4. Using the role assignment and role definition, a new binding is added to the existing bindings. (line 22)
  5. The new role assignment is added to the SPWeb. (line 23)
  6. Everything is updated in the SPWeb

Method: CreateCategoryListInstance

Now that the permissions and groups have been handled, it is time to begin creating and linking content that will be used by this feature.

  1:     Protected Function CreateCategoryListInstance(ByVal web As SPWeb) As SPList
  2:         ' Create a new list for the categories and store the GUID for use later.
  3:         Dim list As SPList
  4:         Dim listID As Guid
  5:         Try
  6:             list = web.Lists("Category List")
  7:             listID = list.ID
  8:         Catch ex As Exception
  9:             list = Nothing
 10:         End Try
 12:         If list Is Nothing Then
 13:             listID = web.Lists.Add("Category List", "List containing categories used in KB Article Creation", "Lists/Cats", "386CF027-6AC7-460a-A7D6-4E503B2B6293", 10800, 100)
 14:         End If
 16:         ' Store the GUID of the new list for clean-up later if needed.
 17:         If web.AllProperties.ContainsKey("KBCategoryList") = True Then
 18:             web.AllProperties("KBCategoryList") = listID.ToString()
 19:         Else
 20:             web.AllProperties.Add("KBCategoryList", listID.ToString())
 21:         End If
 22:         web.Update()
 24:         ' Retrieve the new list and provide some updates.
 25:         If list Is Nothing Then
 26:             list = web.Lists(listID)
 27:         End If
 28:         list.ContentTypesEnabled = True ' Use the specified content type
 29:         list.EnableAttachments = False  ' No attachments
 30:         list.AllowDeletion = False      ' Don't want this one to accidentally get deleted
 31:         list.Hidden = True              ' Don't show this in the view all site content area
 32:         list.NoCrawl = True             ' Don't include items from this list in search results
 33:         list.OnQuickLaunch = False      ' Don't show a link to this list on the quick launch
 34:         list.Update()
 36:         ' Return the list
 37:         Return list
 38:     End Function

Much of this code is already documented with comments. However, here are the raw steps being used.

  1. The existing list is found or a new list is created. (lines 3-14)
  2. The GUID of the list is stored in the web properties (lines 17-22)
    This is done to help other developers on the team quickly locate key lists within the SharePoint implementation. Utilities may be written to update the categories programatically or make changes to existing items.
  3. The properties of the list are updated (line 28-34)

Method: CreateCategoryLookupSiteColumn

Now that the category list has been created, a new site column can be created to perform lookups on the list.

  1:     Protected Function CreateCategoryLookupSiteColumn(ByVal web As SPWeb, ByVal list As SPList) As Boolean
  2:         Try
  3:             ' Define the format for this lookup field
  4:             Dim fieldXMLFormat As String = _
  5:                "<Field ID=""{{3324B767-6CB6-4789-A0ED-60FB39BAC648}}""" & _
  6:                " Name=""SWBTS_KBCategory"" StaticName=""SWBTS_KBCategory""" & _
  7:                " DisplayName=""Category"" Type=""Lookup"" List=""{0}""" & _
  8:                " ShowField=""Title"" LCID=""1033""" & _
  9:                " Group=""Knowledge Base Columns""" & _ 
 10:                " UnlimitedLengthInDocumentLibrary=""false"" />"
 12:             ' Using the format and apply the category list GUID to the string
 13:             Dim fieldXML As String = String.Format(fieldXMLFormat, list.ID.ToString())
 15:             ' Add the field to the site columns collection
 16:             web.Fields.AddFieldAsXml(fieldXML)
 17:             web.Update()
 18:             Return True
 19:         Catch ex As Exception
 20:             Return False
 21:         End Try
 22:     End Function

The easiest way to create a new field is to use the XML markup to define the field. Notice that there is a placeholder on line 7 for the GUID of the list. This is where most of the issues in creating lookup columns in features were experienced by me. Once it was determined that using the list GUID was necessary for the site column to work properly, it was decided that this was the best way to implement the site lookup column (line 13).

The field is then added to the current web context and the web updated with the new site column (lines 16 – 17).

Method: UpdateKBDocumentContentType

Now that the site column has been added containing the categories for the system, the underlying content types need to be updated with a reference to this new site column.

  1:    Protected Function UpdateKBDocumentContentType(ByVal web As SPWeb) As Boolean
  2:         Try
  3:             ' Get the IT Knowledge Base Article content type
  4:             Dim CT As SPContentType = web.ContentTypes("IT Knowledge Base Article")
  6:             ' Create the GUID for the site column to add to the content type
  7:             Dim fieldID As Guid = New Guid("3324B767-6CB6-4789-A0ED-60FB39BAC648")
  9:             ' Add a new link to the site column for the IT Knowledge Base Article content type
 10:             CT.FieldLinks.Add(New SPFieldLink(web.Fields(fieldID)))
 12:             ' Update the content type and all child content types.
 13:             CT.Update(True)
 15:             Return True
 16:         Catch ex As Exception
 17:             Return False
 18:         End Try
 19:     End Function

The content type is located and then a new field link is added to the content type (line 10). The content type is then updated.

Method: CreateKBDocumentLibrary

Finally, all of the pieces are in place to create the document library that will house all of the KB documents. Now, it just needs to be added using the following code:

  1:     Protected Function CreateKBDocumentLibrary(ByVal web As SPWeb) As SPDocumentLibrary
  2:         Dim kbDocLibraryID As Guid
  3:         Dim docLibrary As SPDocumentLibrary
  4:         Try
  5:             docLibrary = web.Lists("Knowledge Base Repository")
  6:             kbDocLibraryID = docLibrary.ID
  7:         Catch ex As Exception
  8:             docLibrary = Nothing
  9:         End Try
 11:         ' Create the new document library and store the guid.
 12:         If docLibrary Is Nothing Then
 13:             Try
 14:                 kbDocLibraryID = web.Lists.Add("KB", "Knowledge base articles and resources.", "KB", "386CF027-6AC7-460a-A7D6-4E503B2B6293", 10801, 101)
 15:             Catch ex As Exception
 16:                 kbDocLibraryID = web.Lists("KB").ID
 17:             End Try
 18:         End If
 20:         ' Store the GUID in the document properties for use when deactivating the feature.
 21:         If web.AllProperties.ContainsKey("KBDocID") Then
 22:             web.AllProperties("KBDocID") = kbDocLibraryID.ToString()
 23:         Else
 24:             web.AllProperties.Add("KBDocID", kbDocLibraryID.ToString())
 25:         End If
 26:         web.Update()
 28:         ' Update the document library and turn on the ability to use content types.
 29:         If docLibrary Is Nothing Then
 30:             docLibrary = web.Lists(kbDocLibraryID)
 31:         End If
 32:         docLibrary.ContentTypesEnabled = True
 33:         docLibrary.Update()
 35:         ' Get a reference for the content types that will be changed.
 36:         Dim docType As SPContentType = docLibrary.ContentTypes("Document")                      ' Document content type declared in the library
 37:         Dim kbType As SPContentType = web.ContentTypes("IT Knowledge Base Article")             ' New IT content type declared in the web site
 38:         Dim hicupType As SPContentType = web.ContentTypes("HICUP Knowledge Base Article")       ' Content Type for old KB Articles
 40:         ' Add the new KB type and remove the generic document type
 41:         Try
 42:             Dim kbDocLibraryType As SPContentType = docLibrary.ContentTypes.Add(kbType)
 43:             Dim hicupDocLibraryType As SPContentType = docLibrary.ContentTypes.Add(hicupType)
 44:             hicupDocLibraryType.Hidden = False
 45:             docLibrary.Update()
 47:             Dim fieldID As Guid = New Guid("3324B767-6CB6-4789-A0ED-60FB39BAC648")
 49:             ' Insure the category column is linked to the document library for this content type
 50:             hicupDocLibraryType.FieldLinks.Add(New SPFieldLink(web.Fields(fieldID)))
 51:             hicupDocLibraryType.Update()
 53:             ' Remove the original document content type -- it isn't needed for this library
 54:             docLibrary.ContentTypes.Delete(docType.Id)
 55:             docLibrary.Update()
 56:         Catch ex As Exception
 57:         End Try
 59:         ' Set some additional properties for the library.
 60:         docLibrary.Title = "Knowledge Base Repository"
 61:         docLibrary.EnableFolderCreation = False                             ' No folder nesting allowed!
 62:         docLibrary.EnableModeration = True                                  ' Items must be approved.
 63:         docLibrary.EnableVersioning = True                                  ' Enable versions for historical tracking
 64:         docLibrary.EnableMinorVersions = False                              ' Do not use minor versions for draft revisions
 65:         docLibrary.DraftVersionVisibility = DraftVisibilityType.Reader      ' Allow everyone to read all documents
 66:         docLibrary.MajorVersionLimit = 8                                    ' Only store the last 8 major versions
 67:         docLibrary.ForceCheckout = True                                     ' You must check out these documents to edit
 68:         docLibrary.OnQuickLaunch = True                                     ' Create a navigational link to the library
 69:         docLibrary.Update()
 71:         ' Update the document library views
 72:         Dim defaultViewFieldsArray() As String = {"DocIcon", "LinkFilename", "SWBTS_KBCategory", "_UIVersionString", "Modified", "Editor", "CheckoutUser"}
 73:         Dim defaultViewFields As New System.Collections.Specialized.StringCollection()
 74:         defaultViewFields.AddRange(defaultViewFieldsArray)
 76:         ' Add new columns to the default view
 77:         Dim defaultView As SPView = docLibrary.DefaultView
 78:         defaultView.ViewFields.DeleteAll()              ' Remove all the old columns, we want to start with a clean view
 79:         For Each fieldName In defaultViewFields         ' Iterate through the defaultViewFields array to add them to the view
 80:             defaultView.ViewFields.Add(fieldName)       ' Add the field to the view
 81:         Next
 82:         ' Change the query
 83:         defaultView.Query = "<GroupBy Collapse=""FALSE"" GroupLimit=""100""><FieldRef Name=""ContentType"" /><FieldRef Name=""_ModerationStatus"" /></GroupBy><OrderBy><FieldRef Name=""FileLeafRef"" /></OrderBy>"
 84:         ' Set a new row limit
 85:         defaultView.RowLimit = 50
 86:         ' Update the default view
 87:         defaultView.Update()
 89:         ' Add a new view to show only the articles transferred from the old system.
 90:         Dim oldItemsView As SPView = docLibrary.Views.Add("KB Articles", defaultViewFields, "<GroupBy Collapse=""FALSE"" GroupLimit=""100""><FieldRef Name=""_ModerationStatus"" /></GroupBy><OrderBy><FieldRef Name=""FileLeafRef"" /></OrderBy><Where><Eq><FieldRef Name=""ContentType"" /><Value Type=""Text"">HICUP Knowledge Base Article</Value></Eq></Where>", 100, True, False)
 91:         oldItemsView.ViewFields.Add("SWBTS_OriginalAuthor")
 92:         oldItemsView.ViewFields.Add("SWBTS_OriginalKBID")
 93:         oldItemsView.ViewFields.Add("SWBTS_OriginalPublished")
 94:         oldItemsView.Update()
 95:         docLibrary.Update()
 97:         ' Add a new view to show articles grouped by category.
 98:         Dim categoryView As SPView = docLibrary.Views.Add("By Category", defaultViewFields, "<GroupBy Collapse=""FALSE"" GroupLimit=""100""><FieldRef Name=""SWBTS_KBCategory"" /></GroupBy><OrderBy><FieldRef Name=""FileLeafRef"" /></OrderBy>", 200, True, False)
 99:         categoryView.ViewFields.Delete("SWBTS_KBCategory")      'Since we are grouping by category, remove the category column from the view
100:         categoryView.Update()
101:         docLibrary.Update()
103:         ' Add a new view to show articles that have been rejected.
104:         Dim rejectedView As SPView = docLibrary.Views.Add("My Rejected Items", defaultViewFields, "<OrderBy><FieldRef Name=""Modified"" Ascending=""FALSE"" /></OrderBy><Where><And><Eq><FieldRef Name=""_ModerationStatus"" /><Value Type=""ModStat"">Rejected</Value></Eq><Eq><FieldRef Name=""Author"" /><Value Type=""Integer""><UserID Type=""Integer"" /></Value></Eq></And></Where>", 15, True, False)
105:         rejectedView.Update()
106:         docLibrary.Update()
108:         ' Add a new view to show articles that need to be reviewed
109:         Dim reviewView As SPView = docLibrary.Views.Add("Needing Review", defaultViewFields, "<OrderBy><FieldRef Name=""SWBTS_ReviewDate"" /></OrderBy><Where><Leq><FieldRef Name=""SWBTS_ReviewDate"" /><Value Type=""DateTime""><Today /></Value></Leq></Where>", 50, True, False)
110:         reviewView.ViewFields.Add("SWBTS_ReviewDate")
111:         reviewView.Update()
112:         docLibrary.Update()
114:         ' Add a new view to show my articles that need to be reviewed
115:         Dim myReviewView As SPView = docLibrary.Views.Add("My Documents to Review", defaultViewFields, "<OrderBy><FieldRef Name=""SWBTS_ReviewDate"" /></OrderBy><Where><And><Or><And><Leq><FieldRef Name=""SWBTS_ReviewDate"" /><Value Type=""DateTime""><Today /></Value></Leq><Eq><FieldRef Name=""Author"" /><Value Type=""Integer""><UserID Type=""Integer"" /></Value></Eq></And><Leq><FieldRef Name=""SWBTS_ReviewDate"" /><Value Type=""DateTime""><Today /></Value></Leq></Or><Eq><FieldRef Name=""Editor"" /><Value Type=""Integer""><UserID Type=""Integer"" /></Value></Eq></And></Where>", 20, True, False)
116:         reviewView.ViewFields.Add("SWBTS_ReviewDate")
117:         myReviewView.Update()
118:         docLibrary.Update()
120:         Return docLibrary
121:     End Function

Admittedly, there is a lot of code above. I would normally stay away from posting this much code, but this also gives an opportunity to show how to do a bunch of things when creating lists and libraries programatically in SharePoint. So, for the broad sweeping brush of what’s happening above:

  1. Find or create the KB Document library. (lines 4-26)
  2. Turn on content types for the library. (lines 29-33)
  3. Add and remove content types. (lines 36-55)
  4. Change some of the settings of the library. (lines 60-69)
  5. Add a bunch of public views for the library. (lines 72-118)

If you follow the comments, you will see a lot of details on what is being done in the code.

No comments: