Tuesday, February 2, 2010

Provisioning Lookup based Site Columns

Scenario:
Finally my client asked for this. He wanted few columns with lookup options.

I will not claim that I wrote the code as I got it from Codeplex but I did made some changes to make sure that solution works well in Edit properties page as well as with Document Information Panel.

Solution:
You need to create a feature for this with 2 element manifest files.

First one will create the list instance of the look up list and add options to it

Second one will have the definition of the Site Column you want to provision and will have a List attribute value set to the list title you have provisioned above. This manifest will not be referenced directly by feature but there will be event receiver which will read this element file and provision the columns using Object Model Code.

Feature Name:
ProvisionLookupSiteColumns , in case you change feature name .. make sure to change it in Code below and feature manifest file.

Feature.xml:

<?xml version="1.0" encoding="utf-8"?>
<Feature Id="New_GUID" 
 Title="Lookup Lists and Content" 
 Description="This feature creates all the lookup lists with initial content."
 Scope="Site"
  ImageUrl="/Client/Logo.png"
  Version="1.0.0.0" 
 Hidden="FALSE" 
 ReceiverAssembly="Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0000000000000000" 
         ReceiverClass="ClientName.ProvisionLookupSiteColumns" >
DefaultResourceFile="core" 
 xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementManifest Location="LookupList.xml" />
   </ElementManifests>
  <Properties>
    <Property Key="ColumnDefinitionPath" Value="SiteColumns.xml" />
  </Properties>
</Feature>
LookList.xml:
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

  <ListInstance 
    FeatureId="00bfea71-de22-43b2-a848-c05709900100" 
    TemplateType="100" 
    Title="Security Classification" 
    Url="Lists/SecurityClassification"
    OnQuickLaunch="FALSE">
    <Data>
      <Rows >
          <Row><Field Name="Title">FOR OFFICIAL USE ONLY</Field></Row>
          <Row><Field Name="Title">Confidential</Field></Row>
          <Row><Field Name="Title">Strictly Confidential</Field></Row>
          <Row><Field Name="Title">Unclassified</Field></Row>
      </Rows>
    </Data>
  </ListInstance>
</Elements>
SiteColumns.xml:
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  
  <Field
      ID="{New_GUID}"
      Name="SecurityClassification"
      DisplayName="Security Classification"
      Type="Lookup"
      Required="TRUE"
      Description="Information about who can access the resource or an indication of its security status."
      Group="Standard"
      List="Security Classification"
      ShowField="LinkTitleNoMenu"
      UnlimitedLengthInDocumentLibrary="FALSE"
    />
    
</Elements>
ProvisionFeatureReceiver.cs:
using System;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;

namespace ClientName
{
    /// <summary>
    /// Creates one or more site columns which gets their data from a SharePoint lists. This is known as a lookup field.
    /// 
    /// Most of the properties of the field(s) are defined in a CAML XML file (located at the value specified 
    /// in the feature property 'ColumnDefinitionPath', however we use the object model to retrieve the GUID of 
    /// the list the field should refer to. Each <Field /> element in the CAML should have a list attribute containing 
    /// the name of the list the column should get data from. The list should exist in the site collection's root 
    /// web (note this could be easily extended). The list GUID is then included in the CAML which is passed to the 
    /// SPWeb.Fields.AddFieldAsXml() method.
    /// 
    /// Note there are some issues surrounding deletion of site columns, see in-line comments for details. Note 
    /// also that the instance of the CAML definition file should stay constant per feature using this technique - 
    /// if new columns are required create a new feature which refers to this FeatureReceiver class, passing 
    /// different parameters from the CAML.
    /// </summary>
    /// <created version="1.0.0.0" by="Chris O'Brien" date="15 April 2007">Sample code</created>
    /// <updated version="1.0.0.1" by="Chris O'Brien" date="11 September 2007">In createLookupColumn(), amended line which 
    /// gets reference to newly-created list to use GetFieldByInternalName() rather than SPWeb.Fields collection. Fix 
    /// suggested by Bruce Sandeman - thanks Bruce!
    /// </updated>

    public class ProvisionLookupSiteColumns : SPFeatureReceiver
    {
        const string FEATURE_FOLDER_NAME = @"Template\Features\ProvisionLookupSiteColumns";
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            // feature is scoped at Site, so the parent is type SPSite rather than SPWeb..
            SPSite site = properties.Feature.Parent as SPSite;
            {
                SPWeb currentWeb = null;
                Guid gRootWebId = Guid.Empty;
                if (site != null)
                {
                    currentWeb = site.RootWeb;
                    gRootWebId = currentWeb.ID;
                }
                else
                {
                    currentWeb = properties.Feature.Parent as SPWeb;
                    gRootWebId = currentWeb.Site.RootWeb.ID;
                }

                using (currentWeb)
                {
                    // read details from CAML..

                    string sFieldElement = null;
                    string sListName = null;

                    string sFilePath = SPUtility.GetGenericSetupPath(FEATURE_FOLDER_NAME);
                    sFilePath += @"\" + properties.Feature.Properties["ColumnDefinitionPath"].Value;

                    XmlTextReader xReader = new XmlTextReader(sFilePath);
                    while (xReader.Read())
                    {
                        if (xReader.LocalName == "Field")
                        {
                            if (xReader.MoveToAttribute("List"))
                            {
                                sListName = xReader.Value;
                                xReader.MoveToElement();
                            }

                            sFieldElement = xReader.ReadOuterXml();

                            // now get reference to list, fix up CAML and create column.. 
                            SPList referencedList = currentWeb.Site.RootWeb.Lists[sListName];

                            string sFinalCaml = replaceListGuidString(sFieldElement, sListName, referencedList);
                            createLookupColumn(currentWeb, sFinalCaml, sListName);
                        }
                    }

                    xReader.Close();

                }
            }
        }

        private string replaceListGuidString(string sFieldElement, string sListName, SPList referencedList)
        {
            string sListWithName = string.Format("List=\"{0}\"", sListName);
            string sListWithGuid = string.Format("List=\"{0}\"", "{" + referencedList.ID + "}");
            return sFieldElement.Replace(sListWithName, sListWithGuid  );
        }

        /// <summary>
        /// Attempt to delete the column. Note that this will fail if the column is in use, 
        /// i.e. it is used in a content type or list. I prefer to not catch the exception 
        /// (though it may be useful to add extra logging), hence feature deactivation/re-activation 
        /// will fail. This effectively means this feature cannot be deactivated whilst the column 
        /// is in use.
        /// </summary>
        /// <param name="column">Column to delete.</param>
        private void attemptColumnDelete(SPWeb web, string sColumnName)
        {
            SPFieldLookup lookupColumn = null;

            // we don't care if there was an exception retrieving the field from the collection, 
            // since if it's not there we can't delete it anyway..
            try
            {
                lookupColumn = web.Fields[sColumnName] as SPFieldLookup;
            }
            catch (ArgumentException argExc)
            {

            }

            if (lookupColumn != null)
            {
                try
                {
                    lookupColumn.Delete();
                }
                catch (SPException SpExc)
                {
                    // consider logging full explanation..

                    throw;
                }
            }
        }

        private void createLookupColumn(SPWeb web, string sColumnDefinitionXml, string sColumnName)
        {
            // delete the column if it exists already and is not yet in use..
            attemptColumnDelete(web, sColumnName);

            SPFieldLookup lookupColumn = null;

            // now create the column from the CAML definition..
            string sCreatedColName = web.Fields.AddFieldAsXml(sColumnDefinitionXml);

            /* COB 11 Sept 2007 - amended next line to use internal name to fix bug when field display name  
             * is different to internal name. */
            // lookupColumn = web.Fields[sCreatedColName] as SPFieldLookup;
            lookupColumn = web.Fields.GetFieldByInternalName(sCreatedColName) as SPFieldLookup;

            lookupColumn.LookupWebId = web.Site.RootWeb.ID;
            lookupColumn.Update();
        }

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            // delete the column if it exists already and is not yet in use..

            // feature is scoped at Site, so the parent is type SPSite rather than SPWeb..
            SPSite site = properties.Feature.Parent as SPSite;
            {
                SPWeb currentWeb = null;
                Guid gRootWebId = Guid.Empty;
                if (site != null)
                {
                    currentWeb = site.RootWeb;
                    gRootWebId = currentWeb.ID;
                }
                else
                {
                    currentWeb = properties.Feature.Parent as SPWeb;
                    gRootWebId = currentWeb.Site.RootWeb.ID;
                }

                using (currentWeb)
                {
                    string sColumnName = null;
                    string sFilePath = SPUtility.GetGenericSetupPath(FEATURE_FOLDER_NAME);
                    sFilePath += @"\" + properties.Feature.Properties["ColumnDefinitionPath"].Value;

                    XmlTextReader xReader = new XmlTextReader(sFilePath);
                    while (xReader.Read())
                    {
                        if (xReader.LocalName == "Field")
                        {
                            if (xReader.MoveToAttribute("Name"))
                            {
                                sColumnName = xReader.Value;
                            }

                            attemptColumnDelete(currentWeb, sColumnName);
                        }
                    }

                    xReader.Close();
                }
            }
        }

        public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        {
        }

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
        {
        }
    }
}

0 comments: