Creating a Unique Document ID provider for SharePoint 2010 based on content type

SharePoint 2010 introduces the Unique Document ID feature, that assigns an unique ID to each document uploaded in the site collection. As said, the scope is site collection, so the document cannot be found outside the site collection using this service.

Out-of-the-box, SharePoint uses its own provider to assign document ID’s. But what if you want to influence the way the unique ID’s are generated? Well, fortunately, SharePoint 2010 also offers the possibility to create your own provider. In this article though, I would like to take it one step further and make the generation of the ID’s dependant on the list item itself. I will use the content type in this example, but you can use any field of the list item you desire.

My case: I have a company that stores contracts and quotes in the same site collection. I have the following rules regarding the unique identifier of the document, also used for reference in correspondence:

  1. A contract should start with CTR
  2. An offer should start with QUO
  3. Any other should start with DOC
  4. It should be followed by universal date notation, meaning year (4), Month (2), day (2), Hour (2), Minutes (2) and Seconds (2)
  5. Then it should contain the customer number, consisting of 6 digits, obtained from the document properties. If the document does not contain a customer number field, or it is empty, we use 000000 as the customer number.

For example: CTR-20100425163145-123456

Create the custom fields and content types

Before we can create a provider based on content types and fields, we first need to define our content type and the customer number field. Right click your project, select add new item and select Content Type in the template window. Name your content type Contract and derive it from Document. Repeat these steps for the Quote content type.

Also add a new Empty Element and call it Fields. Within the Elements.xml file of the Fields element, include a field definition for the customer number field. See below xml definition:

<?xml version=1.0 encoding=utf-8?>
<Elements xmlns=http://schemas.microsoft.com/sharepoint/>
  
<Field
       ID={5744d18c-305e-4632-8bd1-09d134f4830d}

       Type=Text

       Name=CustomerNumber

       DisplayName=Customer Number

       Group=Boom.CustomDocumentIdProvider>
    </Field>
</Elements>

 Now that we have our field, we complete the content types to include this field. Other than the name, both content types are the same for the purpose of this blog. See below definition for the Contract content type:

<?xml version=1.0encoding=utf-8?>
<Elements xmlns=http://schemas.microsoft.com/sharepoint/>
<!– Parent ContentType: Document (0x0101) –>
   <ContentType
      ID=0x010100dc76f46f281449aaa449d8eb30decd03

      Name=Contract

      Group=Boom.CustomDocumentIdProvider

      Description=Contract

      Inherits=TRUE

      Version=0>
     
<FieldRefs>
        
<FieldRef
            ID={5744d18c-305e-4632-8bd1-09d134f4830d}
            Name=CustomerNumber
            DisplayName =Customer Number/>
     
</FieldRefs>
   </ContentType>
</Elements>

 And of course, the definition for the Quote content type:

<?xml version=1.0encoding=utf-8?>
<Elements xmlns=http://schemas.microsoft.com/sharepoint/>
<!– Parent ContentType: Document (0x0101) –>
   <ContentType
     
ID=0x010100ae26a4fc10bc4810bd234d75bb499864
     
Name=Quote
     
Group=Boom.CustomDocumentIdProvider
     
Description=Quote
     
Inherits=TRUE
      Version=0>
     
<FieldRefs>
        
<FieldRef
           
ID={5744d18c-305e-4632-8bd1-09d134f4830d}
            Name=CustomerNumber
            DisplayName =Customer Number/>
       
/FieldRefs>
   
</ContentType>
</Elements>

Now that we have our content types and fields, we can continue with the custom document id provider.

Create the custom document ID provider

 If we want to implement our provider, we need to extend the following class: Microsoft.Office.DocumentManagement.DocumentIdProvider. There are four methods we need to override to implement our provider:

  1. public override string GenerateDocumentId(SPListItem listItem) à returns the new ID
  2. public override bool DoCustomSearchBeforeDefaultSearch() à specifies whether we do a custom search first rather than using the search framework
  3. public override string[] GetDocumentUrlsById(SPSite site, string documentId) à implements our custom lookup method
  4. public override string GetSampleDocumentIdText(SPSite site) à returns an example of the ID’s that will be generated by our provider

Let us start by creating a new empty SharePoint project in Visual Studio 2010.

Once done, create a class called DocumentIdCache and include the code listed above for the cache object. Secondly, add a class called CustomIdProvider. Let the class inherit from the DocumentIdProvider in the Microsoft.Office.DocumentManagement namespace. For this, add a reference to the Microsoft.Office.DocumentManagement assembly that is located on the .NET tab.

Override the three methods and one property from the base class, so that you have the skeletons in your class as below:

namespace Boom.CustomDocumentIdProvider {
   public class CustomIdProvider : Microsoft.Office.DocumentManagement.DocumentIdProvider {
      public override string GenerateDocumentId(Microsoft.SharePoint.SPListItem listItem)
{
         throw new NotImplementedException();
     
}

     public override string[] GetDocumentUrlsById(Microsoft.SharePoint.SPSite site, string documentId) {
        throw new NotImplementedException();
    
}

     public
override string GetSampleDocumentIdText(Microsoft.SharePoint.SPSite site)
{
        throw new NotImplementedException();
     }

     public override bool DoCustomSearchBeforeDefaultSearch
{
        get {
           throw new NotImplementedException();
        }
     
}
  
}
}

We will implement the easy ones first 😉
In the DoCustomSearchBeforeDefaultSearch property, make sure in returns false. This will instruct SharePoint to use the default search method to find the document. Creating a custom loop method is beyond the scope of the blog, but if you have a better way of finding the document, you can do so in the GetDocumentUrlsById method.
In the GetSampleDocumentIdText method, make sure it returns a string that resembles the pattern you will return for your id’s, for example CTR-20100422153421-123456.
The GetDocumentUrlsById method contains our logic to find documents other than search. Because we use the standard search method, we return a new empty string.

Property and methods should now resemble something like below:

public override string GetSampleDocumentIdText(Microsoft.SharePoint.SPSite site) {
    return “CTR-20100422153421-123456”;
}

public override bool DoCustomSearchBeforeDefaultSearch {
   get {

      return false;
  
}
}

public override string[] GetDocumentUrlsById(Microsoft.SharePoint.SPSite site, string documentId) {
   return new string[0];
}

Great, halfway there J. Now we are going to look at generating the Id’s. The ID is generated in the GenerateDocumentId method. See below code snippet:


public override string GenerateDocumentId(Microsoft.SharePoint.SPListItem listItem)
{
   string customerNumber = “000000”;

   // Get current date and time in universal notation

   string dateString = DateTime.Now.Year.ToString() + DateTime.Now.Month.ToString() + DateTime.Now.Day.ToString() + DateTime.Now.Hour + DateTime.Now.Minute + DateTime.Now.Second;

   string contentTypeString = null;

   try
{
      // Get the customer number of the document

      if (listItem.Fields.Contains(new Guid(“5744d18c-305e-4632-8bd1-09d134f4830d”)))
{
         if (listItem[new Guid(“5744d18c-305e-4632-8bd1-09d134f4830d”)] != null)
           
customerNumber = listItem[new Guid(“5744d18c-305e-4632-8bd1-09d134f4830d”)].ToString();
     
}
      // Obtain the content type
      SPContentType documentType = listItem.ContentType;

      // determine identifier

      switch (documentType.Name) {

         case “Contract”: {
            contentTypeString = “CTR”;
            break;
         }
         case “Quote”: {
            contentTypeString = “QUO”;
            break;
         }
         default : {
            contentTypeString = “DOC”;
            break;
        
}
      }

      string idString = string.Format(“{0}-{1}-{2}”, contentTypeString, dateString, customerNumber);
      return idString;
   } catch (Exception ex) {
      return string.Format(“DOC-{0}-{1}”, dateString, customerNumber);
  
}
}

We first check whether our list item contains a field called CustomerNumber (defined in our solution). If the list item contains the field, we check whether or not a value was entered and if so, obtain the value.

We then continue to get the content type of the list item. Based on the content type’s name, we define the first part of our document id.

Finally, we glue all of it together and return the identifier.

Last thing we need to complete now is register the custom provider with our site collection. Right click the features node in the solution explorer and select Add Feature. Right click the created feature and select Add Event Receiver.
We will attach the custom provider in this receiver. Implement the FeatureActivated and FeatureDeactivating events. See below code snippet.

public
override void FeatureActivated(SPFeatureReceiverProperties properties)
{
   SPSite site = (SPSite)properties.Feature.Parent;

   DocumentId.SetProvider(site, new CustomDocumentIdProvider.CustomIdProvider ());
}

public
override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
   SPSite site = (SPSite)properties.Feature.Parent;

   DocumentId.SetDefaultProvider(site);
}

We register our provider by calling the static DocumentId.SetProvider method, passing an instance of our fresh custom Id provider. In the deactivating method, we reset the provider by using the static DocumentId.SetDefaultProvider method.

Your solution explorer should now resemble the following:

I have included the feature receiver in its own feature, to disconnect the provider registration from the type declarations. Your entire package (wsp) should look like below image:

Deploy and test the solution

 

Right click the project node in the solution explorer and select Deploy Solution. Once deployed, navigate to your site and create a document library that contains both additional content types, like below image:

Now test your provider by adding a document, contract and quote content type. Once the provider job has run, each document should have been assigned with its own identifier, like below:

Because I enabled the Document Id Service after I have created the documents, each document has the same date time stamp, because they are all processed in the same batch. But normally, this is not the case.

Final thoughts

Off course, there are some drawbacks to this implementation. For instance, if multiple normal documents were uploaded and the service was turned on at a later stage, they would have the same ID. However, the scope of this blog is just to show that you could create your own provider and that you could use properties of the document and site to generate the identifiers for the document. Most important aspect to keep in mind is that you have to ensure that the generated ID is unique within the site collection.

I have uploaded the Visual Studio 2010 solution here. Good luck with creating your own!

Advertisements

Your search cannot be completed because this site is not assigned to an indexer

I encountered this error when I set up my fresh SharePoint 2010 farm and created the site collection before completing the configuration of the search service.

I checked all my settings, but the search service was up and running and presented no errors. Still, I was not able to fire a query from code, as this exception kept popping up. After doing a search on internet, I found that the problem was not with the search service, but with the content database settings. Strange place I would say. It appears though that each content database is assigned its own indexer and because the search service was turned on after the content database had been created, no indexer was assigned to the content database.

So, to correct this problem, open Central Admin, go to Manage Content Databases, select your content database and select the appropiate indexer in the properties page. Once done, the problem was gone.

Easy enough right?

Using PowerShell 1.0 to change web application setting in SharePoint 2007

Sure, PowerShell is already available for a long time on Windows 2003, also in combination with SharePoint 2007. But untill this time, I could avoid the use 😉

But with PowerShell 2.0 becoming more important to SharePoint 2010, it was time to dive a little bit into this scripting language.
At my current customer, the maximum upload size for documents needed to be increased to 100 MB, from the default setting of 50 Mb. Obviously, we could do this using the Central Admin, but it becomes more of a problem when there are a lot of web applications, hence the choice for scripting. Also, a lot of other settings on different layers need to be adjusted to make this work, for example WebDAV settings in Vista and registry settings for the crawler, but this post only covers the PowerShell script to change the setting in the web application general settings.

So, below is my first PowerShell script. Note that this script is by no means the best one. It could be extended with exception handling, parameters to specify action and size, but for the purpose of this post, it is clear enough.

[Void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
$farm = [Microsoft.SharePoint.Administration.SPFarm]::Local

Write-Host("Get all web applications within each web service")
$websvcs = @($farm.Services | where -FilterScript {$_.GetType() -eq [Microsoft.SharePoint.Administration.SPWebService]})

foreach ($websvc in $websvcs) {
  foreach ($webpp in $websvc.WebApplications) {
    $webapp.MaximumFileSize = 100
    $webapp.Update()
  }
}

Not much exiting stuff going on here right? First I load the Microsoft.SharePoint.dll assembly by calling the LoadWithPartialName method of the System.Reflection.Assembly class. Because this method is static, we use the ‘::’ operator. Once loaded, we get the local farm by calling the SPFarm.Local method. Again, becuase Local is a static method, we use the ‘::’ operator.

When done, we get all SPWebService objects within the Services collection of the Farm. This line is a little less obvious. In C#, we would use the SPFarm.Services.GetValue<SPWebService>() method. In PowerShell, we filter the Services collection by using a sort of SQL like syntax. Get all services where type (GetType()) equals (-eq) Microsoft.SharePoint.Administration.SPWebService. The rest speaks for itself and looks quite a lot like C# code.

There you have it, my first PowerShell 😉 Naturally, we could create a simple command line utility that does the same using C#, but these scripts are created faster and are also easy adjustable to fit needs.

So, in short, with SharePoint 2010 embracing PowerShell, we have no choice then to venture in the world of PowerShell.

Till next time!

Office 2010 and SharePoint 2010 RTM available on MSDN!

Finally, Office 2010 and SharePoint Server 2010 are available on MSDN for MSDN subscribers.

Great achievement guys! Congrats.

Pick up your copy on http://msdn.microsoft.com/subscriptions

SharePoint 2010 will be available to Volume License customers with Software Assurance on April 27th.
Customers without Software Assurance can order their copy on May 1st.
Public release will be on May 12th.

Retail availability is expected to be on June 15th.

Have fun with the best SharePoint version yet!

Migrating to SharePoint 2010 – Some thoughts

With Microsoft SharePoint 2010 approaching for official release on May 12th 2010 (earlier for MSDN, SA, VL licenses), businesses are already looking at how we can easily migrate to 2010.

So is it just click and install? Or are any special arrangements needed? Yesterday, we tested one of our custom extension packs on the new platform (RC).
There is some good news and some bad news 😉 The good news is that features contained in the WSP pack were installed without a problem. The bad news is that some of the features in the pack introduced problems when activated.

Basically, all standard features, like content types, document libraries, list definitions and site definitions worked without a problem. Also the eventhandlers attached were also correctly attached in 2010.
So far so good. Features that were dependant on the 12 hive (for whatever reason, it is obviously not good practice) have a problem as the 12 hive is now called 14 😉

The features that did not work upon install were those that were dependant or interacted with Shared Service Provider components. As the architecture behind these are complete different in SharePoint 2010, I did not expected it to work without a fight anyway 🙂 The move to Service Applications, where the individual components of the Shared Service Provider were disconnected and isolated, caused several of my features to fail, as they could not find the Shared Service Provider. Also, some of the classes used for UserProfiles are now deprecated and replaced.

Another thing I noticed was that when I created a document library from my installed doc lib definition, it would be created, but function like the 2007 version. The row selection, checkboxes and 2010 look and feel were not available, meaning that the xml definition also needs an upgrade if you fully want to utilize the 2010 functions.

In short, some actions are indeed needed, before one could easily move to 2010. For standard sites though, that do not contain any drastic customizations, it is failry easy, using the content database attach method.

Nice day everone!

Extending the Ribbon with a Send Unique Link Button

In one of my previous posts (located here), I discussed the Unique Document ID feature that can be enabled on all document libraries within the site collection. In that post, I also mentioned that it was strange that the E-mail a Link button on the ribbon then still uses the actual url of the document, instead of the virtualized unique url of the document. Why would you have a unique url to a document that you cannot easily send?

So, I decided to add my own button to do so. In the end, it is not that difficult, but a lot of new concepts of SharePoint 2010 can be caught in this single venture. To name a few, Extending the Ribbon interface, the Client Object Model (javascript) and Custom Actions. So, without further due, let us continue.

The Approach

We need to accomplish several things to make all of this work.

  1. Add a button to the ribbon in the context of a document library.
  2. Assign a custom action to execute when the button is clicked.
  3. Obtain the selected item from the document library.
  4. Query the server to get the unique document url of the selected document
  5. Open a mail message containing a description (subject) and the unique url.
  6. Finally, make the button context ‘aware’, so that it is only enabled when a document is selected.

So, let us start with the first part, add a button to click.

Add a button to the ribbon

Let us examine the end state first. We would like a button to be added to the ribbon next to the original E-mail a Link button, like in the image below:

The ribbon is composed using a XML definition that contains three logical parts: Tabs, Groups and Controls. These are hierarchical, meaning that Tabs contain Groups and Groups contain Controls. A tab (for example Documents) or group (for example New) can be static (always available) or contextual.

In our case, we want to add our button (control) to the Share & Track group on the Documents tab. This marks our Location, which we will see later when we start coding. To be exact, the location of our control will be Ribbon.Documents.Share.Controls, where Controls is the container of the group Share on the tab Documents that is located on the ribbon. Sounds logical enough right?

So, when we want to add something on the ribbon (where we can also create our own tabs, groups and contextual controls), we first need to find out the location. Based on the convention, it would be easy enough to figure out, but sometimes the naming is not exactly equal to that displayed in the UI. The following msdn article mentions all default locations for the ribbon, but an excessive overview is beyond the scope of this blog. http://msdn.microsoft.com/en-us/library/bb802730.aspx

Let’s start coding 😉 Open Visual Studio 2010 (this can also be done with VS 2008, but you must then build the package yourself. Visual Studio has far better build-in support for SharePoint 2010. Create a new empty SharePoint project and give it a name, in my case I use Boom.EmailUniqueLinkButton.

Enter the url of the site you want to debug to and use deploy as a farm solution option. Once done, right click the created project and add a new item to the project. Use the Empty Element type and give it a meaningful name, in my case EmailUniqueLinkButton.

Right click again on the project and add SharePoint “Layouts” Mapped Folder. This folder will contain the resources that will be used by our button, in our case the javascript. Also notice that a subfolder with the name of the project is automatically added beneath the Layouts folder. If not, you should add it manually. Right click the feature beneath the feature node (feature 1) and select rename. Rename it to a meaningful name, in my case Boom.EmailUniqueLink. If desired, you can change the default title in the properties also. The default scope of our feature is Web. We will keep it at that scope. When done, our solution will look like below image.

The elements.xml file will contain the xml definition needed to add our button to the ribbon. Open the file and add the following node to the xml (as child of Elements):

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   <CustomAction
      
Id="Ribbon.EmailUniqueLink"
      
Location="CommandUI.Ribbon"
      
RegistrationId="101"
      
RegistrationType="List">
      
<CommandUIExtension>
         
<CommandUIDefinitions>
         
</CommandUIDefinitions>
         
<CommandUIHandlers>
         
</CommandUIHandlers>
      
</CommandUIExtension>
  
</CustomAction>
</Elements>
 

This needs some clarification. Beneath Elements, we define a CustomAction, which has the following attributes:

  • Id – This will uniquely identify our action, can be anything, but is usually something that could easily be related to the ribbon.
  • Location – This indicates where we want to add our custom action, in our case to the CommandUI.Ribbon. Mark though that this is not yet the location of our button! That will follow later.
  • RegistrationType – Although marked in the documentation as optional, it is not. RegistrationType indicates the type to which this action is bound. Could be List, FileType, ContentType or ProgId. In out case, it is List.
  • RegistrationId – Although marked in the documentation as optional, it is not. RegistrationId indicates the type id to which this action is bound. In our case the list type id (101 = Document Library). In case it was a content type, this would contain the content type id.

Below the CustomAction element, we can define a CommandUIExtension, which contains a CommandUIDefinitions and CommandUIHandlers. The former specifies what will be added, the latter specifies what happens when the control is used (in our case clicked).

Include a CommandUIDefinition node below CommandUIDefinitions. Add an attribute Location. The location will dictate where the control will be added. In our case, Ribbon.Documents.Share.Controls._children. Note the ‘_children’ behind Controls, which indicates to SharePoint that a new child should be added to the Controls collection. Now, finally we can add our button. Include the following xml beneath the CommandUIDefinition node.


<Button
  
Id="Ribbon.Documents.Share.EmailUniqueLink"
  
Command="Ribbon.Documents.Share.EmailUniqueLink"
  
Sequence="15"
  
Image16by16="/_layouts/$Resources:core,Language;/images/formatmap16x16.png"
  
Image16by16Top="-16"
   Image16by16Left="-88"
  
Image32by32="/_layouts/$Resources:core,Language;/images/formatmap32x32.png"
   Image32by32Top="-128"
   Image32by32Left="-448"
  
Description="Sends the unique link to the document by email"
  
LabelText="E-mail Unique Link"
  
ToolTipTitle="E-mail Unique Link"
  
ToolTipDescription="Sends the unique link to the document by e-mail"
  
TemplateAlias="o1"
/>

Before we get to the attributes, I wanted to reuse the image that was used for the original E-mail button. Partly because it would save me work, but more over because I am terrible in creating images and icons 😉 Secondly, I needed to check the Sequence, which defines the order in which the controls are displayed on the ribbon. As I wanted my new button to appear next to the original one, the sequence should be higher than the first, but lower than the following control. To check this, we can take a look at the default Ribbon definition located at 14\Template\Global\XML\CMDUI.xml. Search for Ribbon.Documents.Share.Controls, which defines the controls in the group we want to add to. There you can find the original EmailItemLink button that is the basis of our control.

<Button
  
Id=Ribbon.Documents.Share.EmailItemLink
  
Sequence=10
  
Command=EmailLink
  
Image16by16=/_layouts/$Resources:core,Language;/images/formatmap16x16.png
   Image16by16Top=-16
   Image16by16Left=-88
  
Image32by32=/_layouts/$Resources:core,Language;/images/formatmap32x32.png
   Image32by32Top=-128
   Image32by32Left=-448
  
LabelText=$Resources:core,cui_ButEmailLink;
  
ToolTipTitle=$Resources:core,cui_ButEmailLink;
  
ToolTipDescription=$Resources:core,cui_STT_ButEmailLinkDocument;
  
TemplateAlias=o1
/>

Three attributes are very important to notice. Sequence, Command and TemplateAlias. The sequence of the original E-mail button is 10. The next control in the list (AlertMe) has a sequence of 20. We therefore choose 15 as the sequence for our control, so that it would still be possible to squeeze items in to both left and right in the future. The Command attribute defines the name of the command that is executed when clicked. We will define that later. The TemplateAlias defines the template for handling scaling and sizing. In order for our control to act like the other controls in the group, we have to choose the same template. Finally, we copy over the image references so that we can use the images that came out of the box. So let us now discuss all the attributes:

  • Id – Marks the unique id of our control. Usually, the convention is to follow the location naming followed by a meaningful name of the control, in our case EmailUniqueLink.
  • Sequence – The order in which the control should be shown. In our case 15.
  • Command – The name of the command to execute. Again, could be anything, but has to be unique, which is why I again use the naming convention.
  • ImageXX – The several image attributes define the images to use for both 16 px and 32 px and their locations. I used the same as the original one taken from the CMDUI.xml file.
  • Description – Describes the control.
  • LabelText – Defines what is shown beneath the button in the UI.
  • ToolTipTitle/ToolTipDescription– Shows the title and description when hovering over the button.

Now we have the button defined, but unless we define the action, it will do nothing. So, add the following xml snippet in the CommandUIHandlers section.

<CommandUIHandler
  
Command=Ribbon.Documents.Share.EmailUniqueLink
  
CommandAction=javascript:EmailUniqueLink();
  
EnabledScript=javascript:EnableEmailUniqueLink();
/>

Note that the Command attribute equals that defined in the button command attribute. In the CommandAction attribute, we define the action to be taken when pressed. In our case, we call a javascript function called EmailUniqueLink that will do the hard work for us later on. You can also define the function and related code inline in this attribute, but I rather have it in a separate .js file, both for easy debugging and maintenance. The final attribute here is called EnabledScript which defines the script that determines whether the button is enabled or not. The called function has to return a boolean. We will define the function later.

We have now defined the button and the action to be taken, but not yet the location of the javascript functions that will be called. Right click the Boom.EmailUniqueLinkButton folder in the layouts folder of the solution explorer and add a .js file. You can call it anyway you want, just remember the name. I call it Boom.EmailUniqueLinkButton.js. We will talk about the code in there later on. Go back to the Elements.xml file. Add another CustomAction element beneath the Elements node.

<CustomAction
  
Id=Ribbon.Documents.Share.EmailUniqueLink.Script
  
Location=ScriptLink
  
ScriptSrc =/_layouts/Boom.EmailUniqueLinkButton/Boom.EmailUniqueLinkButton.js
/>

Again, the Id marks the unique identifier for this action. In the Location attribute, we specify ‘ScriptLink’ to indicate that it concerns an external file. In the ScriptSrc attribute, we specify the location of our just created javascript file.

Your Elements.xml file should now look like this:

<?xml version=1.0“vencoding=utf-8?>
<Elements xmlns=http://schemas.microsoft.com/sharepoint/>
<CustomAction
  
Id=Ribbon.EmailUniqueLink
  
Location=CommandUI.Ribbon
  
RegistrationId=101
  
RegistrationType=List>
  
<CommandUIExtension>
     
<CommandUIDefinitions>
        
<CommandUIDefinition
           
Location=Ribbon.Documents.Share.Controls._children>
           
<Button
              
Id=Ribbon.Documents.Share.EmailUniqueLink
              
Command=Ribbon.Documents.Share.EmailUniqueLink
              
Sequence=15
              
Image16by16=/_layouts/$Resources:core,Language;/images/formatmap16x16.png
               Image16by16Top=-16
               Image16by16Left=-88
               
Image32by32=/_layouts/$Resources:core,Language;/images/formatmap32x32.png
               Image32by32Top=-128
               Image32by32Left=-448
              
Description=Sends the unique link to the document by e-mail
              
LabelText=E-mail Unique Link
               
ToolTipTitle=E-mail Unique Link
               
ToolTipDescription=Sends the unique link to the document by e-mail
              
TemplateAlias=o1/>
         
</CommandUIDefinition>
     
</CommandUIDefinitions>
     
<CommandUIHandlers>
        
<CommandUIHandler
           
Command=Ribbon.Documents.Share.EmailUniqueLink
           
CommandAction=javascript:EmailUniqueLink();
           
EnabledScript=javascript:EnableEmailUniqueLink();/>
      
</CommandUIHandlers>
   </CommandUIExtension>
</CustomAction>
<CustomAction
  
Id=Ribbon.Documents.Share.EmailUniqueLink.Script
  
Location=ScriptLink
  
ScriptSrc =/_layouts/Boom.EmailUniqueLinkButton/Boom.EmailUniqueLinkButton.js/>
</Elements>

We have now defined our button and performed the necessary wiring to make it respond to actions. Now let’s look at the code that will use the Client Object Model to get the unique document id link. Don’t worry, we are almost there 😉

Using the Client Object Model to get List Item properties

SharePoint 2010 adds the Client Object Model to our toolbox, which we can use through native code, Silverlight or javascript. (ECMAScript). Discussing the inner workings of the client object model is beyond the scope of this blog. We will only use the model to get the properties of the list item.

Many of the internet examples cover the Silverlight or native side of the client object model. I will use the ECMAScript method to get the properties of the list item and use it to generate the mail message.

So let us take a look at the javascript code. Open the added js file and include function bodies for the methods defined in the Elements.xml file, like below snippet.
function EmailUniqueLink() {
}

// Delegate that is called when server operation is complete upon success.
function onQuerySucceeded(sender, args) {
}

// Delegate that is called when server operation is completed with errors.
function onQueryFailed(sender, args) {
}

// Method to enable/disable the e-mail unique button on the ribbon.
function EnableEmailUniqueLink() {
}

// This method will contain most of the code needed to request the unique url to the document

The first method contains most of the code. The onQuerySucceeded and onQueryFailed methods are delegates that are needed because the client object model in javascript works asynchronously. The last method is needed to specify when the button is enabled.

When using the client object model, we always have to obtain the client context first. The client context contains the proxies that are used to connect to the server. Second thing we need to know is that the client object model works with batches. This means that we load the object we want returned in the context and then execute it first. Include the following code snippet in the EmailUniqueLink method.


// First get the context and web
var ctx = SP.ClientContext.get_current();
this.web = ctx.get_web();// Get the current selected list, then load the list using the getById method of Web (SPWeb)
var listId = SP.ListOperation.Selection.getSelectedList();
var sdlist = this.web.get_lists().getById(listId);

In this code snippet, we first obtain the client context and load the web. Then we use the SP.ListOperation.Selection object to get the current list. This will return the GUID of the list, which we then use to get the list reference through the web object. Please mark though that the web and list objects are not populated yet. We again use the Sp.ListOperation.Selection object to get the current selected item in the list, which will return an object containing an id property. Using that id, we can request the list item from the list. To have the list item populated and thus get our unique document id, we need to load it in the context and then execute the query. As said, this can only be done asynchronously. This is done by calling executeQueryAsync and pass the success and fail methods respectively. Another thing to note here is that we use this when we need to access the variable outside the method. Include the following snippet in the onQuerySucceeded method.


// Request url by using the get_item method. It will return the Url field type, which has a Url property.
var url = this.listItem.get_item('_dlc_DocIdUrl').get_url();
// Request the name of the document.
var title = this.listItem.get_item('FileLeafRef');
// Open a new e-mail in the default mail program.
window.open('mailto:?subject=Emailing%3A%20'+ title + '&body=' + url);


Once the asynchronous call is done, we can access the properties of the listitem by calling the get_item() method passing the name of the field you wish to return, in this case the ‘_dlc_DocIdUrl‘ that contains the unique link. Because that returns a SPFieldUrl type like object, we call get_url() to get the actual url. To complete the mail message, we also request the name of the file and then call the mailto: to open the message in an e-mail client.
In this case, we requested the entire listitem object from the server. We can also specify which properties to return so we can limit bandwidth use and increase performance. The way to do this is by adding the desired properties as parameters in the load method of the context, in our case _dlc_DocIdUrl and FileLeafRef, like below snippet.


// Only request name and DocIdUrl

ctx.load(this.listItem, '_dlc_DocIdUrl', 'FileLeafRef');


I prefer to load the entire object during debugging, so I can inspect the entire object (using developer tools >IE7).

// Get the currently selected item of the list. This will return a dicustonary with an id field
var items = SP.ListOperation.Selection.getSelectedItems(ctx);
var mijnid = items[0];

// Request the list item from the server using the getItemById method. This will load all properties.
// If needed, one could pre-request the fields to be loaded to preserve bandwidth.
this.listItem = sdlist.getItemById(mijnid.id);
// load the item in the context for batch operation.
ctx.load(this.listItem);

//Execute the actual script on the server side. Specify delegates to handle the response.
ctx.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));

The failed body is not of much interest, although you could add code there to shown that it went wrong and why, as it gets the arguments containing the error passed. Add the following snippet to the failed method.


alert('failed ' + args.toString());


That is all that is needed to create the e-mail and send the unique url. Last method only enables the button when there is actually an item selected. Add the following snippet to the EnableEmailUniqueLink method:


// request number of selected items.
var items = SP.ListOperation.Selection.getSelectedItems();
var count = CountDictionary(items);
// only return true is a single item is selected.
return (count == 1);


Above is quite self-explanatory I guess. I get the number of selected items and only return true if the number of selected items equals 1. The entire javascript file should now resemble the following:


/*
====================================================================================================================
File: Boom.EmailUniqueLinkButton.js
Description: Contains supporting javascript functions to allow the unique link of a document to be sent by e-mail
Date:19-04-2010
Author: Patrick Boom
====================================================================================================================
*/

// This method will contain most of the code needed to request the unique url to the document
function EmailUniqueLink() {
   // First get the context and web
   var ctx = SP.ClientContext.get_current();
   this.web = ctx.get_web();
   // Get the current selected list, then load the list using the getById method of Web (SPWeb)
   var listId = SP.ListOperation.Selection.getSelectedList();
   var sdlist = this.web.get_lists().getById(listId);
   // Get the currently selected item of the list. This will return a dicustonary with an id field
   var items = SP.ListOperation.Selection.getSelectedItems(ctx);
   var mijnid = items[0];
   // Request the list item from the server using the getItemById method. This will load all properties.  
   // If needed, one could pre-request the fields to be loaded to preserve bandwidth.
   this.listItem = sdlist.getItemById(mijnid.id);
   // load the item in the context for batch operation.
   ctx.load(this.listItem);
   //Execute the actual script on the server side. Specify delegates to handle the response.
   ctx.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));
}

// Delegate that is called when server operation is complete upon success.
function onQuerySucceeded(sender, args) {
   // Request url by using the get_item method. It will return the Url field type, which has a Url property.
   var url = this.listItem.get_item('_dlc_DocIdUrl').get_url();

   // Request the name of the document.

   var title = this.listItem.get_item('FileLeafRef');

   // Open a new e-mail in the default mail program.
   window.open('mailto:?subject=Emailing%3A%20' + title + '&body=' + url);
}

// Delegate that is called when server operation is completed with errors.
function onQueryFailed(sender, args) {
   alert('failed ' + args.toString());
}

// Method to enable/disable the e-mail unique button on the ribbon.

function EnableEmailUniqueLink() {
   // request number of selected items.
   var items = SP.ListOperation.Selection.getSelectedItems();
   var count = CountDictionary(items);

   // only return true is a single item is selected.

   return (count == 1);
}

Only thing left to do is compile and deploy the code. If you followed all steps correctly you should see a button next to the E-mail a Link button and when you click it (and you have the Document ID feature enabled in the Site features), an e-mail should open with the unique link in the body, like below image:

That’s it, watch the fruit of all you hard labor. In this post, we covered quite some material, like customizing the ribbon and using the client object model to examine the selected list item. Hope you like it! You can download the entire VS solution from here. Leave a comment if you have a couple of minutes. Much appreciated!

See ya…