Share via


Enabling File Names In Lookup Fields

For my first post, I figured that I would leverage a prototype that I worked on last week. I was investigating some of the issues that one of the Fab40 templates has after you upgrade the server from 2007 to 2010. I learned that the Knowledge Base template has a novel way of solving one of the problems that lookup fields have when using a document library.

Before we dig into the solution to the problem, I should first spend some time describing the problem.

When you create a lookup field, you need to choose a field from the “other” list to be used as the display value. When you are editing an item from “this” list, you will be shown a drop-down list box that allows you to choose an item from the “other” list. When each of the items from the other list are added to the drop-down list, some text value from the other item needs to be displayed here that uniquely identifies the item. Some fields work better than other fields.

For example, let’s say that you are adding a lookup field that is going to be used to select someone from a list of contacts. Using the Zip Code as the field that is displayed in the drop-down list isn’t likely to be very helpful. It would be extremely common for you to have multiple contacts that are in the same zip code. You would then see the same zip code listed more than once in the drop-down list and you would have no idea which entry represents which contact. Choose a contact would be a pretty frustrating experience. Using a field like Name would be a lot better. Granted, the Name field could also contain duplicates. But it would still be a whole lot better than a field like Zip Code which is likely to contain many duplicates.

The problem we have is when the “other” list is a document library. Which field should you choose for the lookup field to display? You could use the Title field but it doesn’t always get populated. Upload some text files into a document library and they will all wind up with an empty value for the Title field. Even if a Title value is specified for each document, this field allows duplicates which, as described above, is not good. So, is there anything about the documents in the document library that is normally going to be unique? Well, the file name. Duh! However, try creating a lookup field to a document library and you’ll notice that there is no file name field to choose. Doh!

The file name is exposed through the object model as a field named FileLeafRef. So why isn't this field available to use in lookup fields? I’m still trying to find someone that can explain that to me. If I figure it out I’ll update this post.

Ok, so now we can (finally) get back to the Knowledge Base site template that I was looking into and how it solved this problem.

One of the features that the Knowledge Base site template has is that articles written in the wiki library can each have one or more related articles. This was accomplished using a multi-value lookup field. Now, multi-valued lookup fields don’t exactly have the greatest UI experience. But they do allow the KB article author to accomplish the scenario without the KB site template developer having to invest in custom (read: expensive) list item selection UI.

But if we're using a multi-value lookup field to choose a document from a document library, what was the field that was used as the lookup field? Well, what the KB site template did was to add a text field to the document library called “Name” in conjunction with an event handler that would ensure that the value of this field always contained the actual name of the file. As wiki articles were created or modified, the event handler would always ensure that the Name field contained the right value.

Now, the way that the KB site template did this was specific to this site template and isn’t really reusable in other site templates. The concept is reusable but the specific implementation in that site template isn’t. So I created a sandboxed solution that contains a reusable implementation of this concept.

Using this solution is pretty easy:

  • Upload the solution - Upload the solution to the Solution Gallery in your site collection and activate it.
  • Activate the site feature - In each of the sites where you want to use this File Name field, activate the site feature named “BillGr's File Name Field”.
  • Add the site column - In each document library where you want to use this field, add the site column named File Name to either the document library or the content types that are used in the document library.

Solution Implementation

In this solution, you’ll find three things:

  • A site column that defines the File Name field.
  • An assembly that contains a sandboxed event handler.
  • A feature (which can be activated/deactivated) with two element manifests:
    • One that defines the File Name field.
    • One that wires-up the event handler to the ItemAdding and ItemUpdating events.

“File Name” Site Column Definition

One thing to note about the site column is that it has been defined with a very unique internal name (the Name attribute). This is to deal with field name collisions. What if a user has already defined a File Name column? What if they already have other data in that column? What if they already have business logic that is driven by the value of that field. My event handler can’t simply look for the first field named “File Name” that it finds and blindly overwrite its value with the current file name. This is one of the reasons why a field definition has both an internal name and a display name. As an aside, the other main reason for internal names is so that compiled code doesn’t need to be changed when the name of a field is localized into another language. But that’s a subject for a different post.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="https://schemas.microsoft.com/sharepoint/">
  3.  
  4.   <!-- NOTE: The Name attribute here must match the value of the FileNameFieldInternalName property
  5.        of the ListItemEvents class that is in ListItemEvents\ListItemEvents.cs -->
  6.   <Field ID="{1511BF28-A787-4061-B2E1-71F64CC93FD5}"
  7.        Name="BillGrFileNameField_FileName"
  8.        DisplayName="File Name"
  9.        Type="Text"
  10.        Required="FALSE"
  11.        Group="Custom Columns">
  12.   </Field>
  13.  
  14. </Elements>

In this case, we’ve given our field an internal name that uses the solution name as a prefix. It is highly unlikely that another solution will define another field with this same internal name. Later, our event handler will use the same internal name when accessing the field value from the list item.

Event Handler Wire-Up

When a feature definition includes an event handler, usually the event handler has a ListTemplateId or ListUrl attribute. These attributes are normally used to narrow down the scope of the event handler to a specific list type. Since I haven’t included any of these attributes, my event handler will be globally registered. It will be called whenever any item in a list or document library is added or changed – anywhere in the site.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="https://schemas.microsoft.com/sharepoint/">
  3.   <Receivers>
  4.       <Receiver>
  5.         <Name>ListItemEventsItemAdding</Name>
  6.         <Type>ItemAdding</Type>
  7.         <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
  8.         <Class>BillGr.Samples.FileNameField.ListItemEvents</Class>
  9.         <SequenceNumber>10000</SequenceNumber>
  10.       </Receiver>
  11.       <Receiver>
  12.         <Name>ListItemEventsItemUpdating</Name>
  13.         <Type>ItemUpdating</Type>
  14.         <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
  15.         <Class>BillGr.Samples.FileNameField.ListItemEvents</Class>
  16.         <SequenceNumber>10000</SequenceNumber>
  17.       </Receiver>
  18.   </Receivers>
  19. </Elements>

Event Handler Implementation

The event handler simply ensures that the value of the File Name field is correct given the current name of the document. The code relatively simple. Whenever we’ve been notified that an item has changed, we first have to confirm that the item is in a document library. If it is a “regular” list item, then it won't have a file so there isn’t any file name to stay in sync with. Now, please, no arguments here about Attachments <grin />. SharePoint treats the primary file stream of a document library item very differently from an attachment to a list item.

In addition to confirming that the item is in a document library, the event handler also needs to confirm that the item has a “File Name” field. If it doesn’t have the field then, again, there isn’t anything that we need to do.

Once we’ve ruled out these other scenarios, all that remains is to extract the file name from the URL and then ensure that the value of the “File Name” field contains this same file name.

  1. using System;
  2. using System.IO;
  3. using System.Security.Permissions;
  4. using Microsoft.SharePoint;
  5. using Microsoft.SharePoint.Security;
  6. using Microsoft.SharePoint.Utilities;
  7. using Microsoft.SharePoint.Workflow;
  8.  
  9. namespace BillGr.Samples.FileNameField
  10. {
  11.     /// <summary>
  12.     /// List Item Events
  13.     /// </summary>
  14.     public class ListItemEvents : SPItemEventReceiver
  15.     {
  16.  
  17.         /// <summary>
  18.         /// Gets a string containing the internal name of the "File Name" field.
  19.         /// </summary>
  20.         public static string FileNameFieldInternalName
  21.         {
  22.             get
  23.             {
  24.                 // NOTE: The value used here has to match the value of the Name attribute in the field
  25.                 // definition that is in FileNameSharedField\Elements.xml.
  26.                 return "BillGrFileNameField_FileName"; // Ensure that this is globally unique by using a prefix that is not likely to be used by someone else.
  27.             }
  28.         }
  29.  
  30.         /// <summary>
  31.         /// An item is being added.
  32.         /// </summary>
  33.         /// <param name="properties">An SPItemEventProperties object that represents properties of the event.</param>
  34.         public override void ItemAdding(SPItemEventProperties properties)
  35.         {
  36.             ListItemEvents.UpdateFileNameField(properties);
  37.             properties.Status = SPEventReceiverStatus.Continue;
  38.         }
  39.  
  40.         /// <summary>
  41.         /// An item is being updated.
  42.         /// </summary>
  43.         /// <param name="properties">An SPItemEventProperties object that represents properties of the event.</param>
  44.         public override void ItemUpdating(SPItemEventProperties properties)
  45.         {
  46.             ListItemEvents.UpdateFileNameField(properties);
  47.             properties.Status = SPEventReceiverStatus.Continue;
  48.         }
  49.  
  50.         /// <summary>
  51.         /// Helper method that ensures that the "File Name" field contains the correct value for the specified list item.
  52.         /// </summary>
  53.         /// <param name="properties">An SPItemEventProperties object that represents properties of the event.</param>
  54.         private static void UpdateFileNameField(SPItemEventProperties properties)
  55.         {
  56.             // We can only do this if the item is in some form of a doc-lib and the content type of
  57.             // the item includes the "File Name" field.
  58.             if ((properties.ListItem == null) ||
  59.                 (properties.ListItem.File == null) ||
  60.                 (!properties.ListItem.Fields.ContainsField(ListItemEvents.FileNameFieldInternalName)))
  61.             {
  62.                 return;
  63.             }
  64.  
  65.             // Extract just the file name from the server-relative URL. Note: URLs with forward slashes
  66.             // are compatible with the System.IO.Path class. We are using that here so that we don't
  67.             // have to do any URL parsing. Any time that we try and do URL parsing on our own we
  68.             // eventually get into trouble beause of some corner case that wasn't accounted for in
  69.             // the parsing code. We'll avoid that problem if we leverage the Path class which has
  70.             // already been extensively tested.
  71.             string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(properties.AfterUrl);
  72.  
  73.             // Ensure that the "File Name" field contains the current file name.
  74.             properties.AfterProperties[ListItemEvents.FileNameFieldInternalName] = fileNameWithoutExtension;
  75.         }
  76.  
  77.     } // class ListItemEvents
  78.  
  79. }

Downloading The Sample

There are two downloads available from the MSDN Code Gallery for this sample:

  • SharePoint Solution - This is a working .WSP that you can upload to the Solution Gallery for your site collection and activate it. Once you do this, the “BillGr's File Name Field” feature will be available to activate in all sites of your site collection. After activating the feature, you can add the site column named “File Name” to a document library to start getting updated file names. You'll find this site column in the “Custom Columns” group.
  • Visual Studio Project - This is the VS 2010 SharePoint solution that was used to generate the solution above.

As with any of my other samples, you are free to use the solution in your site or take the source code and leverage it in some other solution that you are working on. Just keep in mind that this is a sample and, as such, has no warranty, no guarantee that it will work in all environments, and no promise that a future version of SharePoint won't break it.