Archive

Archive for the ‘Object Model’ Category

SharePoint: How to get value from BDC

December 13th, 2011 No comments

    I’ve implemented a few classes to simplify an access to BDC meta data and values from external data sources. By means of these classes you can make request for values from external data source, using a BDC Entity Instance identifier(s) or a value of a certain BDC Entity field.

The root class BDCMetaRequest allows to interact with BDC as a meta data store. The main goal of the BDCMetaRequest is to get a meta descriptor of a certain external data type registered in BDC. The found descriptor is an object of the Entity class and can be accessed through the FoundEntity property.

A base abstract class BDCRequest derived from BDCMetaRequest contains some general members responsible for searching the external data item that meets criteria and for fetching any field value out from the found item. It’s supposed that the criteria and criteria-specific search implementation are encapsulated inside derived classes. The BDCRequest supplies the following important members:

  • FoundEntityInstance returns the external data item that meets criteria. In terms of BDC the found data item is a BDC Entity Instance;
  • GetBdcEntityInstance returns the found data item as DataTable with one row. The row represents either the record found in database or the object returned by a Web service;
  • GetBdcEntityInstanceFieldValue returns a value of a certain field. The field value is being extracted from the found data item;

*Note that all classes described in this post actively use the lazy loading, so the searching for external data item will be performed only if it hasn’t been found before.

As I mentioned above, the derived classes BDCRequestById and BDCRequestByValue are responsible for different search criteria and criteria-specific search implementation. The BDCRequestById accepts the identifier(s) of a sought-for BDC Entity Instance in the form of an encoded string or array of objects. While the BDCRequestByValue accepts a value, which will be applied to the first available BDC Entity filter (WildcardFilter or ComparisonFilter). BDCRequestByValue acts just as the user who, interacting with Picker Dialog, chooses the filter, types the required value in the proper text field and presses the Search button (see picture below).

Sample of Dialog Picker

Ok, below is the classes and a digram of them:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Portal.WebControls;
using Microsoft.Office.Server.ApplicationRegistry.Infrastructure;
using Microsoft.Office.Server.ApplicationRegistry.MetadataModel;
using Microsoft.Office.Server.ApplicationRegistry.Runtime;
using Microsoft.Office.Server;
using System.Data;

namespace BDC
{
    /// <summary>
    /// Abstracts an interaction with BDC meta data
    /// </summary>
    public class BDCMetaRequest
    {
        #region fields & properties
        protected string _lobSystemInstanceName = null;
        protected string _bdcEntityName         = null;

        protected LobSystemInstance _lobSystemInstance = null;
        protected Entity            _entity            = null;

        /// <summary>
        /// Name of the requested Lob System Instance
        /// </summary>
        public string RequestedLobSystemInstanceName
        {
            get { return _lobSystemInstanceName; }
        }
        /// <summary>
        /// Name of the requested external data type
        /// </summary>
        public string RequestedEntityName
        {
            get { return _bdcEntityName; }
        }
        /// <summary>
        /// Found meta descriptor of external data type 
        /// </summary>
        public virtual Entity FoundEntity
        {
            get { return _entity == null ? (_entity = GetEntity()) : _entity; }
        }
        /// <summary>
        /// Found Lob System Instance
        /// </summary>
        public virtual LobSystemInstance FoundLobSystemInstance
        {
            get { return _lobSystemInstance == null ? (_lobSystemInstance = GetLobSystemInstance()) : _lobSystemInstance; }
        }
        #endregion

        #region public methods
        /// <summary>
        /// Initializes a new instance of the BDCMetaRequest class
        /// </summary>
        /// <param name="lobSysInstanceName">Name of the requested Lob System Instance</param>
        /// <param name="bdcEntityName">Name of the requested external data type</param>        
        public BDCMetaRequest(string lobSysInstanceName, string bdcEntityName)
        {
            // check if lobSysInstanceName isn't NULL or empty string
            EnsureParamIsNotNullOrEmpty("lobSysInstanceName", lobSysInstanceName);
            // check if bdcEntityName isn't NULL or empty string
            EnsureParamIsNotNullOrEmpty("bdcEntityName", bdcEntityName);

            _lobSystemInstanceName = lobSysInstanceName;
            _bdcEntityName         = bdcEntityName;
        }
        /// <summary>
        /// Initializes a new instance of the BDCMetaRequest class
        /// </summary>
        /// <param name="businessDataField">SharePoint field of BusinessData type</param>        
        public BDCMetaRequest(BusinessDataField businessDataField)
        {
            // check if businessDataField isn't NULL
            EnsureParamIsNotNull("businessDataField", businessDataField);

            _lobSystemInstanceName = businessDataField.SystemInstanceName;
            _bdcEntityName         = businessDataField.EntityName;
        }
        #endregion

        #region internal methods
        /// <summary>
        /// Gets the Entity object from BDC that meets criteria
        /// </summary>
        /// <returns>Found BDC Entity</returns>
        protected Entity GetEntity()
        {
            // get BDC Entity from Lob System Instance by name
            return FoundLobSystemInstance.GetEntities()[RequestedEntityName];
        }
        /// <summary>
        /// Gets the LobSystemInstance from BDC that meets criteria
        /// </summary>
        /// <returns>Found LobSystemInstance</returns>
        protected LobSystemInstance GetLobSystemInstance()
        {
            // get Lob System Instance by name
            return ApplicationRegistry.GetLobSystemInstanceByName(RequestedLobSystemInstanceName);
        }
        /// <summary>
        /// Throws an exception if the passed parameter value is NULL or empty string
        /// </summary>
        /// <param name="paramName">Parameter name</param>
        /// <param name="value">Parameter value</param>
        protected static void EnsureParamIsNotNullOrEmpty(string paramName, string value)
        {
            if (string.IsNullOrEmpty(value))
                throw new ArgumentException(string.Format("{0} must not be NULL or empty!", paramName), paramName);
        }
        /// <summary>
        /// Throws an exception if the passed parameter value is NULL
        /// </summary>
        /// <param name="paramName">Parameter name</param>
        /// <param name="value">Parameter value</param>
        protected static void EnsureParamIsNotNull(string paramName, object value)
        {
            if (value == null)
                throw new ArgumentNullException(paramName, string.Format("{0} must not be NULL!", paramName));
        }
        #endregion
    }

    /// <summary>
    /// Abstracts an interaction with BDC
    /// </summary>
    public abstract class BDCRequest : BDCMetaRequest
    {
        protected bool            _formattedValues = false;
        protected IEntityInstance _iEntityInstance = null;
        
        /// <summary>
        /// Indicates if result values will be formatted
        /// </summary>
        public bool FormattedValues
        {
            get { return _formattedValues; }
            set { _formattedValues = value; }
        }

        /// <summary>
        /// Gets found data item from external datasource
        /// </summary>
        public IEntityInstance FoundEntityInstance
        {
            get { return _iEntityInstance == null ? (_iEntityInstance = GetEntityInstance()) : _iEntityInstance; }
        }

        /// <summary>
        /// Initializes a new instance of the BDCRequest class
        /// </summary>
        /// <param name="lobSysInstanceName">Name of the Lob System Instance</param>
        /// <param name="bdcEntityName">Type name of returned Business Data Object</param>
        /// <remarks>This constructor is called by derived class constructors</remarks>
        public BDCRequest(string lobSysInstanceName, string bdcEntityName) : base(lobSysInstanceName, bdcEntityName)
        {            
        }
        /// <summary>
        /// Initializes a new instance of the BDCRequest class
        /// </summary>
        /// <param name="businessDataField">SharePoint field of BusinessData type</param>
        /// <remarks>This constructor is called by derived class constructors</remarks>
        public BDCRequest(BusinessDataField businessDataField) : base(businessDataField)
        {            
        }

        /// <summary>
        /// Returns a DataTable with one row, which represents either a record from a database or an object returned by a Web service
        /// </summary>
        /// <returns>DataTable with one row as the found data item from external data source</returns>
        public virtual DataTable GetBdcEntityInstance()
        {
            IEntityInstance entityInstance = FoundEntityInstance;
            return FormattedValues ? entityInstance.EntityAsFormattedDataTable : entityInstance.EntityAsDataTable;
        }
        /// <summary>
        /// Returns a value of certain field of the data item found in external data source
        /// </summary>
        /// <param name="bdcEntityFieldName">Field name</param>
        /// <returns>Field value</returns>
        public virtual object GetBdcEntityInstanceFieldValue(string bdcEntityFieldName)
        {
            if (!string.IsNullOrEmpty(bdcEntityFieldName))
            {
                IEntityInstance entityInstance = FoundEntityInstance;
                return GetFieldValue(entityInstance, bdcEntityFieldName);
            }
            return null;
        }

        /// <summary>
        /// Gets BDC Entity Instance that meets criteria
        /// </summary>
        /// <returns>Found BDC Entity Instance</returns>
        protected abstract IEntityInstance GetEntityInstance();

        /// <summary>
        /// Gets a value of certain field of BDC Entity Instance
        /// </summary>
        /// <param name="entityInstance">BDC Entity Instance</param>
        /// <param name="bdcEntityFieldName">Field name</param>
        /// <returns>Field value</returns>
        protected virtual object GetFieldValue(IEntityInstance entityInstance, string bdcEntityFieldName)
        {
            foreach (Field field in entityInstance.ViewDefinition.Fields)
                if (bdcEntityFieldName.Equals(field.Name, StringComparison.OrdinalIgnoreCase) ||
                   bdcEntityFieldName.Equals(field.DefaultDisplayName, StringComparison.OrdinalIgnoreCase) ||
                   (field.ContainsLocalizedDisplayName && bdcEntityFieldName.Equals(field.LocalizedDisplayName, StringComparison.OrdinalIgnoreCase)))
                    return FormattedValues ? entityInstance.GetFormatted(field.Name) : entityInstance[field.Name];
            return null;
        }        
    }

    /// <summary>
    /// Represents search of data item in external data source, using identifier(s)
    /// </summary>
    public class BDCRequestById : BDCRequest
    {
        protected string _bdcEntityInstanceEncodedId = null;
        protected object[] _bdcEntityInstanceIds = null;

        /// <summary>
        /// Encoded identifier(s) of data item
        /// </summary>
        public string BdcEntityInstanceEncodedId
        {
            get { return _bdcEntityInstanceEncodedId; }
        }
        /// <summary>
        /// Array containing identifier(s) of data item
        /// </summary>
        public object[] BdcEntityInstanceIds
        {
            get { return _bdcEntityInstanceIds; }
        }

        /// <summary>
        /// Initializes a new instance of the BDCRequestById class
        /// </summary>
        /// <param name="lobSysInstanceName">Name of the Lob System Instance</param>
        /// <param name="bdcEntityName">Type name of returned Business Data Object</param>
        /// <param name="bdcEntityInstanceEncId">Encoded identifier(s) of data item</param>
        public BDCRequestById(string lobSysInstanceName, string bdcEntityName, string bdcEntityInstanceEncId)
            : base(lobSysInstanceName, bdcEntityName)
        {
            // check if bdcEntityInstanceEncId is a valid encoded identifier(s)
            EnsureParamIsEncodedIdentifier("bdcEntityInstanceEncId", bdcEntityInstanceEncId);

            _bdcEntityInstanceEncodedId = bdcEntityInstanceEncId;

            // get decoded version of passed encoded identifier(s)
            _bdcEntityInstanceIds = EntityInstanceIdEncoder.DecodeEntityInstanceId(_bdcEntityInstanceEncodedId);
        }

        /// <summary>
        /// Initializes a new instance of the BDCRequestById class
        /// </summary>
        /// <param name="businessDataField">SharePoint field of BusinessData type</param>
        /// <param name="bdcEntityInstanceEncId">Encoded identifier(s) of data item</param>
        public BDCRequestById(BusinessDataField businessDataField, string bdcEntityInstanceEncId)
            : base(businessDataField)
        {
            // check if bdcEntityInstanceEncId is a valid encoded identifier(s)
            EnsureParamIsEncodedIdentifier("bdcEntityInstanceEncId", bdcEntityInstanceEncId);

            _bdcEntityInstanceEncodedId = bdcEntityInstanceEncId;

            // get decoded version of passed encoded identifier(s)
            _bdcEntityInstanceIds = EntityInstanceIdEncoder.DecodeEntityInstanceId(_bdcEntityInstanceEncodedId);
        }

        /// <summary>
        /// Initializes a new instance of the BDCRequestById class
        /// </summary>
        /// <param name="lobSysInstanceName">Name of the Lob System Instance</param>
        /// <param name="bdcEntityName">Type name of returned Business Data Object</param>
        /// <param name="bdcEntityInstanceIds">Array containing identifier(s) of data item</param>
        public BDCRequestById(string lobSysInstanceName, string bdcEntityName, object[] bdcEntityInstanceIds)
            : base(lobSysInstanceName, bdcEntityName)
        {
            // check if array bdcEntityInstanceIds isn't NULL and empty
            EnsureArrayIsNotNullOrEmpty("bdcEntityInstanceIds", bdcEntityInstanceIds);

            _bdcEntityInstanceIds = bdcEntityInstanceIds;

            // get encoded version of passed identifier(s)
            _bdcEntityInstanceEncodedId = EntityInstanceIdEncoder.EncodeEntityInstanceId(bdcEntityInstanceIds);
        }

        /// <summary>
        /// Initializes a new instance of the BDCRequestById class
        /// </summary>
        /// <param name="businessDataField">SharePoint field of BusinessData type</param>
        /// <param name="bdcEntityInstanceIds">Array containing identifier(s) of data item</param>
        public BDCRequestById(BusinessDataField businessDataField, object[] bdcEntityInstanceIds)
            : base(businessDataField)
        {
            // check if array bdcEntityInstanceIds isn't NULL and empty
            EnsureArrayIsNotNullOrEmpty("bdcEntityInstanceIds", bdcEntityInstanceIds);

            _bdcEntityInstanceIds = bdcEntityInstanceIds;

            // get encoded version of passed identifier(s)
            _bdcEntityInstanceEncodedId = EntityInstanceIdEncoder.EncodeEntityInstanceId(bdcEntityInstanceIds);
        }

        /// <summary>
        /// Throws an exception if the passed parameter value is a valid encoded identifier(s)
        /// </summary>
        /// <param name="paramName">Parameter name</param>
        /// <param name="value">Parameter value</param>
        public static void EnsureParamIsEncodedIdentifier(string paramName, string value)
        {
            if (string.IsNullOrEmpty(value) || !EntityInstanceIdEncoder.IsEncodedIdentifier(value))
                throw new ArgumentException("Invalid encoded identifier!", paramName);
        }

        /// <summary>
        /// Throws an exception if the passed parameter value isn't NULL and empty array
        /// </summary>
        /// <param name="paramName">Parameter name</param>
        /// <param name="value">Parameter value</param>
        public static void EnsureArrayIsNotNullOrEmpty(string paramName, object[] value)
        {
            if (value == null || value.Length == 0)
                throw new ArgumentException("Array must not be NULL or empty!", paramName);
        }

        /// <summary>
        /// Gets BDC Entity Instance that meets identifier(s)
        /// </summary>
        /// <returns>Found BDC Entity Instance</returns>
        protected override IEntityInstance GetEntityInstance()
        {
            // get BDC Entity Instance by its identifier(s)
            return FoundEntity.FindSpecific(BdcEntityInstanceIds, FoundLobSystemInstance);
        }
    }

    /// <summary>
    /// Represents search of data item in external data source by basing on a certain field value. 
    /// The value has to belong to the field used in item picker in UI
    /// </summary>
    public class BDCRequestByValue : BDCRequest
    {
        protected string _value = null;

        /// <summary>
        /// Field value to search data item by
        /// </summary>
        public string BdcEntityInstanceFieldValue
        {
            get { return _value; }
        }

        /// <summary>
        /// Initializes a new instance of the BDCRequestByValue class
        /// </summary>
        /// <param name="lobSysInstanceName">Name of the Lob System Instance</param>
        /// <param name="bdcEntityName">Type name of returned Business Data Object</param>
        /// <param name="bdcEntityInstanceFieldValue">Field value to search data item by</param>
        public BDCRequestByValue(string lobSysInstanceName, string bdcEntityName, string bdcEntityInstanceFieldValue)
            : base(lobSysInstanceName, bdcEntityName)
        {
            // check if bdcEntityInstanceFieldValue isn't NULL and empty string
            EnsureParamIsNotNullOrEmpty("bdcEntityInstanceFieldValue", bdcEntityInstanceFieldValue);                    

            _value = bdcEntityInstanceFieldValue;
        }

        /// <summary>
        /// Initializes a new instance of the BDCRequestByValue class
        /// </summary>
        /// <param name="businessDataField">SharePoint field of BusinessData type</param>
        /// <param name="bdcEntityInstanceFieldValue">Field value to search data item by</param>
        public BDCRequestByValue(BusinessDataField businessDataField, string bdcEntityInstanceFieldValue)
            : base(businessDataField)
        {
            // check if bdcEntityInstanceFieldValue isn't NULL and empty string
            EnsureParamIsNotNullOrEmpty("bdcEntityInstanceFieldValue", bdcEntityInstanceFieldValue);

            _value = bdcEntityInstanceFieldValue;
        }

        /// <summary>
        /// Gets BDC Entity Instance that meets the field value
        /// </summary>
        /// <returns>Found BDC Entity Instance</returns>
        protected override IEntityInstance GetEntityInstance()
        {            
            // search the entity using the first filter of available ones
            FilterCollection fc = FoundEntity.GetFinderFilters();
            if (fc[0] is WildcardFilter)
                ((WildcardFilter)fc[0]).Value = BdcEntityInstanceFieldValue; //"%" + request.BdcEntityInstanceFieldValue + "%";
            else if (fc[0] is ComparisonFilter)
                ((ComparisonFilter)fc[0]).Value = BdcEntityInstanceFieldValue;

            // find suitable BDC Entity instances and take the first one
            IEntityInstanceEnumerator entityInstanceEnumerator = FoundEntity.FindFiltered(fc, FoundLobSystemInstance);
            entityInstanceEnumerator.MoveNext();
            return entityInstanceEnumerator.Current;
        }
    }
}

BDC Request Class Diagram

There is an ability to get formatted values by setting FormattedValues = true, however, complex formatting slows performance, so use it only if really necessary.

How to use the classes:

BusinessDataField businessDataField = null;

// usage of BDCRequestByValue
BDCRequestByValue requestByValue = new BDCRequestByValue("ExternalProductDB_Instance", "Products", "Microsoft Office");
DataTable theFirstFoundRecord = requestByValue.GetBdcEntityInstance();
object producer = theFirstFoundRecord.Rows[0]["Producer"];
object price    = theFirstFoundRecord.Rows[0]["Price"];

object fieldValue = requestByValue.GetBdcEntityInstanceFieldValue("Producer");

requestByValue = new BDCRequestByValue(businessDataField, "Microsoft Office") { FormattedValues = true };
theFirstFoundRecord = requestByValue.GetBdcEntityInstance();
fieldValue = requestByValue.GetBdcEntityInstanceFieldValue("Producer");

// usage of BDCRequestById
BDCRequestById requestById = new BDCRequestById("ExternalProductDB_Instance", "Products", "__dk410035008400140025005400k410...");
theFirstFoundRecord = requestById.GetBdcEntityInstance();
producer = theFirstFoundRecord.Rows[0]["Producer"];
price    = theFirstFoundRecord.Rows[0]["Price"];

fieldValue = requestById.GetBdcEntityInstanceFieldValue("Producer");

requestById = new BDCRequestById("ExternalProductDB_Instance", "Products", new object[] { 2 });
theFirstFoundRecord = requestById.GetBdcEntityInstance();

requestById = new BDCRequestById(businessDataField, new object[] { 2 });
theFirstFoundRecord = requestById.GetBdcEntityInstance();

SharePoint: Understanding BusinessData Column (BDC Field)

December 6th, 2011 No comments

    This is the logical continuation of the article SharePoint: Brief introduction to Business Data Catalog (BDC)

Business Data Catalog (BDC) provides SharePoint with an ability to display, choose and store data from such external data sources as Web services and DataBases. Data item from external data source (either a record from a database or an object returned by a Web service) might be called BDC Entity Instance. BDC Column (or BDC Field, they are synonyms) is a column of ‘Business Data’ type. Once BDC Column is added to a list, users will be able to choose a BDC Entity Instance from the BDC to store as the value of the BDC Column.

BDC Column can be considered as a complex field, because technically it’s a group of several logically related SP fields. Adding one ‘Business Data’ column to the list, at least two fields will be added to the list in fact. The first is intended to contain a display value that will be shown to the user, while the second is intended to store an encoded identifier of the chosen BDC Entity Instance.

Let’s take a look at an example. Let’s assume that we have a Business Data Catalog that provides us with the Product object with the following structure:

Product 
{
	ID,
	Name,
	Price,
	Producer
}

If we create a new ‘Business Data’ column on a list as it shown in the picture below,
Add Business Data Column
the list schema exposes the following new fields:

<Field Type="BusinessData" DisplayName="Product" 
Required="FALSE" ID="{bc203358-6113-470f-9b08-f6100cc034f2}" 
StaticName="Product" BaseRenderingType="Text" Name="Product" 
SystemInstance="ExternalProductDB_Instance" Entity="Products" 
BdcField="Name" Profile="" HasActions="False" 
SecondaryFieldBdcNames="Price:Producer" RelatedField="Products_ID" 
SecondaryFieldWssNames="Product_x003a__x0020_Price:Product_x003a__x0020_Producer" 
RelatedFieldBDCField="" RelatedFieldWssStaticName="Products_ID" 
SecondaryFieldsWssStaticNames="Product_x003a__x0020_Price:Product_x003a__x0020_Producer" />
  
<Field Type="Note" DisplayName="Products_ID" Hidden="TRUE" ReadOnly="TRUE"
BdcField="Products_ID" ID="{0d37c424-0e57-4429-8f92-0b8faec5a5bd}" 
StaticName="Products_ID" Name="Products_ID" />

<Field Type="Note" DisplayName="Product: Price" ReadOnly="TRUE" 
BdcField="Price" ID="{c56d7123-cb3f-46f4-8f45-874ac5cee13d}" 
StaticName="Product_x003a__x0020_Price" Name="Product_x003a__x0020_Price" />

<Field Type="Note" DisplayName="Product: Producer" ReadOnly="TRUE" 
BdcField="Producer" ID="{df740ff6-2998-46e1-ac40-974d66ba7dc7}" 
StaticName="Product_x003a__x0020_Producer" Name="Product_x003a__x0020_Producer" />

*Note: some redundant attributes of fields are skipped

We have four added SP Fields that are logically related. As I mentioned before, the first field (Name=”Product”) is intended to store a value, which is displayed to the user. Besides, the field contains a number of attributes that refer to all of the rest of logically related fields. We’ll study these attributes a bit later. The second field (Name=”Products_ID”) is a hidden field, which is intended to store an encoded identifier of the BDC Entity Instance. The encoded identifier usually looks like “__dk410035008400140025005400k410…”. It’s permissible if more than one BDC Entity fields compose the identifier.

Other two additional fields (Name=”Product_x003a__x0020_Price” and Name=”Product_x003a__x0020_Producer”) are to store and show such properties of the chosen BDC Entity Instance as Price and Producer. These additional fields are optional, you might be unwilling to have any extra information in the list.

Ok, let’s examine closer the first field (Name=”Product”). It includes the attributes that describe what properties of the chosen BDC Entity Instance will be captured and what additional SP fields they will be stored in. The most important attributes are shown below:

  • SystemInstance – the name of the Lob System Instance or Business Data Catalog Application Instance, which BDC Column is bound to;
  • Entity – the type name of returned business data object, which BDC Column is bound to;
  • BdcField – the name of BDC Entity field, the value of which will be stored in this SharePoint field. For the Product field specifically, the stored value will be displayed to the user;

  • RelatedField – the internal name of a hidden SharePoint field, which is to store an encoded identifier of the chosen BDC Entity Instance;
  • RelatedFieldBDCField – usually an empty string, probably, it’s reserved for future usage, or it’s a legacy attribute;
  • RelatedFieldWssStaticName – the static name of a hidden SharePoint field, which stores an encoded identifier of the chosen BDC Entity Instance;

  • SecondaryFieldBdcNames – the colon-separated names of the fields within BDC Entity, the values of those fields are to be stored in the additional SharePoint fields. The attribute contains the same number of names as the SecondaryFieldWssNames attribute;
  • SecondaryFieldWssNames – the colon-separated internal names of the additional SharePoint fields, that are to store additional values from the chosen BDC Entity Instance. The attribute contains the same number of names as the SecondaryFieldBdcNames attribute;
  • SecondaryFieldsWssStaticNames – the colon-separated static names of the additional SharePoint fields, that are to store additional values from the chosen BDC Entity Instance. The attribute contains the same number of names as the SecondaryFieldBdcNames and SecondaryFieldWssNames attributes;

All of the rest of fields have BdcField-attributes as well. The attribute contains the name of BDC Entity field, the value of which will be stored in corresponding SharePoint field. But I’ve found an exception. This exception is the hidden field, which is intended to store an encoded identifier. In the given case it’s the Products_ID field. The BDCField-attribute of such fields always contains an invalid value, which equals to the name of the field itself. In our case is BdcField=”Products_ID”, and there is no BDC Entity field with the name Products_ID defined either in DataBase or Application Definition File. And the same situation is with all BusinessDate SharePoint fields I investigated.

Below is a summary schema that exposes what BusinessData column attributes refer to

BDC Field Schema Explanation

To work with BusinessData field programmatically we can cast SPField-object to BusinessDataField type defined in the Microsoft.SharePoint.Portal.WebControls namespace:

using Microsoft.SharePoint.Portal.WebControls;
...
SPList spList = ...
SPField spField = spList.Fields["display name of some field"];
BusinessDataField bizDataField = (BusinessDataField)spField;

To get values of attributes and to get all logically related SharePoint fields we can use the following code:

...
string systemInstance = bizDataField.SystemInstanceName;
string entity         = bizDataField.EntityName;
string bdcField       = bizDataField.BdcFieldName;

// get info about related field, which is to store the encoded identifier of BDC Entity Instance
string relatedField              = bizDataField.RelatedField;
string relatedFieldBDCField      = bizDataField.GetProperty("RelatedFieldBDCField");
string relatedFieldWssStaticName = bizDataField.GetProperty("RelatedFieldWssStaticName");

// get info about additional fields, that are to store additional values from BDC Entity Instance
string[] secondaryFieldBdcNames = bizDataField.GetSecondaryFieldsNames();

string[] separators = new string[] { ":" };

string secondaryFieldWssNamesVal = bizDataField.GetProperty("SecondaryFieldWssNames");
string[] secondaryFieldWssNames = string.IsNullOrEmpty(secondaryFieldWssNamesVal) ? 
    new string[0] :
    secondaryFieldWssNamesVal.Split(separators, StringSplitOptions.RemoveEmptyEntries);

string secondaryFieldsWssStaticNamesVal = bizDataField.GetProperty("SecondaryFieldsWssStaticNames");
string[] secondaryFieldsWssStaticNames = string.IsNullOrEmpty(secondaryFieldsWssStaticNamesVal) ?
    new string[0] :
    secondaryFieldsWssStaticNamesVal.Split(separators, StringSplitOptions.RemoveEmptyEntries);
        
// get all logically related sp fields
SPField relatedSPField = spList.Fields.GetFieldByInternalName(relatedField);
List<SPField> secondarySPFields = new List<SPField>(secondaryFieldWssNames.Length);
foreach (string secondaryFieldName in secondaryFieldWssNames)
    secondarySPFields.Add(spList.Fields.GetFieldByInternalName(secondaryFieldName));

SharePoint: Brief introduction to Business Data Catalog (BDC)

November 4th, 2011 No comments

    Business Data Catalog (BDC) allows to integrate business data from external business applications into SharePoint application. BDC has built-in support for displaying data from such data sources as databases and web services. In other words, if the business application is a database or comprises a web service to emit data, its data can be easily incorporated into SharePoint application. If some business application isn’t database and doesn’t have any proper web services, you always can develop your own web service to provide access to the business application’s data.

Once the external business application has been registered within Business Data Catalog, its data can be used in Business Data Web Parts, Business Data Column, Search, User profile and other custom solutions.

BDC provides access to the underlying data sources with a declarative Metadata Model. The Metadata Model allows to describe a simplified and consistent client object model over any business application. What is Metadata? Metadata describes the business applications’ APIs. For each business application, Metadata defines the business entities that the business application interacts with and the methods available in the business application. The Business Data Catalog stores the metadata in the metadata repository. So, using the Metadata Model, developer describes the API of the business application in a xml-file, so called Application Definition File. Then SharePoint administrator imports the Application Definition into the Business Data Catalog to register Metadata the file contains. After that the data from business application becomes available. The schematic structure of Application Definition File is shown below:

<LobSystem Type="..." Name="...">
  <LobSystemInstances>
    <LobSystemInstance Name="...">
      <Properties>
        ...
      </Properties>
    </LobSystemInstance>
  </LobSystemInstances>
  <Entities>
    <Entity Name="...">
      <Properties>
        ...
      </Properties>
      <Identifiers>
        ...
      </Identifiers>
      <Methods>
        ...
      </Methods>
    </Entity>
  </Entities>
</LobSystem>

*Note: some attributes are skipped

The LobSystem is a container for all of the other objects in the Metadata object model, and the root node in the Application Definition File. Each LobSystem object has an unique name and is of a certain type: either Database or Web Service. LobSystem object contains LobSystemInstances and Entities. The LobSystemInstance contains properties that define the authentication of the connection and the provider, which is used to connect to the external data source. Entity defines a type of returned business data object, it contains identifiers, methods and actions. Also Entity can have other related entities associated with them.

In terms of SharePoint Central Administration, the LobSystem is a Business Data Catalog Application or BDC Application, while LobSystemInstance can be named Business Data Catalog Application Instance or BDC Application Instance. Entity in SharePoint Central Administration and in Metadata Model means the same.

BDC Metadata Model Schema

All BDC Applications registered in Business Data Catalog can be viewed through the Central Administration: open SharePoint 3.0 Central Administration, click the name of Shared Service Provider (in my case it’s SharedServices1), then click the View applications link in the Business Data Catalog section. To create Application Definition File and import it to the Business Data Catalog, read the very good article written by Tobias Zimmergren.

To work with BDC programmatically you should use types from the following namespaces:

using Microsoft.Office.Server;
using Microsoft.Office.Server.ApplicationRegistry.MetadataModel;
using Microsoft.Office.Server.ApplicationRegistry.Infrastructure;
using Microsoft.Office.Server.ApplicationRegistry.Runtime;

You mainly will use the next interfaces and objects:

namespace Microsoft.Office.Server.ApplicationRegistry.MetadataModel
{
    // Provides access to all of the LOB systems and LOB system instances registered in the Business Data Catalog
    public sealed class ApplicationRegistry { ... }

    // Represents a business application registered in the Business Data Catalog
    public class LobSystem : MetadataObject { ... }

    // Represents an instance of a business application registered in the Business Data Catalog
    public class LobSystemInstance : MetadataObject { ... }

    // Represents a type of returned business data object, contains identifiers, methods and actions
    public class Entity : DataClass { ... }
}

namespace Microsoft.Office.Server.ApplicationRegistry.Runtime
{
    // Represents a filter that limits the instances returned to those that meet the comparison operator condition
    public class ComparisonFilter : UserInputFilter { ... }

    // Represents a filter that limits the instances returned to those where field like value, where value may contain the asterisk (*) wildcard character
    public class WildcardFilter : ComparisonFilter { ... }

    // Represents instances of business objects
    public interface IEntityInstance : IInstance { ... }

    // Provides a single iteration over the entity instance collection
    public interface IEntityInstanceEnumerator : IEnumerator<IEntityInstance> { ... }
}

namespace Microsoft.Office.Server.ApplicationRegistry.Infrastructure
{
    // Provides encoding and decoding of entity instance identifiers
    public static class EntityInstanceIdEncoder { ... }

    // Represents the SQL session provider to connect to the Shared Services Provider database
    public sealed class SqlSessionProvider { ... }
}

The Business Data Catalog is implemented as a Microsoft Office SharePoint Server 2007 Shared Service. If you are going to deal with BDC inside a standalone window/console-based application or inside SharePoint Jobs, you have to prepare Shared Resource Provider to use in the Object Model. For that, you need to invoke the method SqlSessionProvider.Instance().SetSharedResourceProviderToUse:

namespace Microsoft.Office.Server.ApplicationRegistry.Infrastructure
{
    public sealed class SqlSessionProvider
    {
        // some methods are omitted
        public void SetSharedResourceProviderToUse(string sharedResourceProviderName);
        public void SetSharedResourceProviderToUse(ServerContext serverContext);        
    }
}

The first variant of the method accepts a name of Shared Resource Provider. In terms of SharePoint Central Administration, the method requires the name of Shared Service. You can see all deployed Shared Services through the Central Administration: open SharePoint 3.0 Central Administration, find the Shared Services Administration section inside the left-side navigation bar and look through the available Shared Services. One of them is a default Shared Service. Another way is to get programmatically the name of Shared Service, which serves your web application. To learn more, please read the following article – How to get Shared Service name.

The second variant of the method use an instance of ServerContext. Below is two auxiliary methods I usually use as wrappers to SetSharedResourceProviderToUse:

public static void PrepareSharedServices(SPSite spSite)
{
    ServerContext sc = ServerContext.GetContext(spSite);
    SqlSessionProvider.Instance().SetSharedResourceProviderToUse(sc);
}

public static void PrepareSharedServices(string sharedServicesName)
{
    SqlSessionProvider.Instance().SetSharedResourceProviderToUse(sharedServicesName);    
}

Do not use this method inside your SharePoint web application, otherwise an exception will be thrown. Because in web context the Business Data Catalog uses the default Shared Services Provider automatically. As I said above, use SetSharedResourceProviderToUse only inside standalone non-web applications or SP Jobs.

SharePoint: How to add Content Type programmatically

September 16th, 2011 No comments

     To add a new Content Type through the SharePoint Object Model I’ve developed a few auxiliary methods and classes. The main class is ContentType class. It contains information used for specifying the required properties of the SPContentType object being added.

public class ContentType
{
    public string         ParentContentTypeName { get; set; }
    public string         Name                  { get; set; }
    public List<FieldRef> FieldRefs             { get; set; }

    public ContentTypeFormUrls   FormUrls       { get; set; }
    public ContentTypeProperties Properties     { get; set; }
    public SPContentTypeId       Id             { get; set; }
}

The FieldRefs property represents a collection of fields that have to be added to the Content Type. The other property names are self-descriptive.

The ContentType class makes reference to classes like FieldRef, ContentTypeFormUrls and ContentTypeProperties.

public class FieldRef
{
    public Guid?  ID                 { get; set; }
    public string Name               { get; set; }
    public string DisplayName        { get; set; }
    public bool?  IsRequired         { get; set; }
    public bool?  IsHidden           { get; set; }
    public bool?  IsReadOnly         { get; set; }

    public string Aggregation        { get; set; }
    public string Customization      { get; set; }
    public string PIAttribute        { get; set; }
    public string PITarget           { get; set; }
    public string PrimaryPIAttribute { get; set; }
    public string PrimaryPITarget    { get; set; }
    public string Node               { get; set; }
}

public class ContentTypeFormUrls
{
    public string DisplayFormUrl { get; set; }
    public string NewFormUrl     { get; set; }
    public string EditFormUrl    { get; set; }
}

public class ContentTypeProperties
{    
    public string Description                 { get; set; }
    public string Group                       { get; set; }
    public bool?  IsHidden                    { get; set; }
    public bool?  IsReadOnly                  { get; set; }
    public string NewDocumentControl          { get; set; }
    public bool?  RequireClientRenderingOnNew { get; set; }
    public bool?  IsSealed                    { get; set; }
}

The main method is AddContentType:

public static void AddContentType(SPWeb spWeb, SPList spList, ContentType contentType)
{
    // get appropriate parent content type
    SPContentType parentContentType = spWeb.AvailableContentTypes[contentType.ParentContentTypeName];
    if (parentContentType != null)
    {
        if (spList.ContentTypes[contentType.Name] == null)
        {
            spList.ContentTypesEnabled = true;

            // create new content type
            SPContentType spContentType = new SPContentType(parentContentType, spList.ContentTypes, contentType.Name);
            // set content type id
            SetContentTypeId(spContentType, contentType.Id);
            // set content type properties
            SetContentTypeProperties(spContentType, contentType.Properties);

            // add fields to conent type
            foreach (FieldRef fieldRef in contentType.FieldRefs)
            {
                if (spList.Fields.ContainsField(fieldRef.Name))
                {
                    SPField targetField = fieldRef.ID.HasValue ? spList.Fields[fieldRef.ID.Value] : spList.Fields.GetFieldByInternalName(fieldRef.Name);
                    if (targetField != null && !spContentType.Fields.ContainsField(fieldRef.Name))
                    {
                        SPFieldLink fLink = new SPFieldLink(targetField);
                        SetFieldRefProperties(fLink, fieldRef);
                        spContentType.FieldLinks.Add(fLink);
                    }
                }
                else
                    Console.Write(string.Format("Couldn't find field {0} in the list {1}", fieldRef.Name, spList.Title));
            }

            // set content type form urls
            SetContentTypeFormUrls(spContentType, contentType.FormUrls);

            // save changes
            spList.ContentTypes.Add(spContentType);
            spContentType.Update();
            spList.Update();
        }
        else
            Console.Write(string.Format("Content type {0} already exists in the list {1}", contentType.Name, spList.Title));
    }
    else
        Console.Write(string.Format("Couldn't find the parent content type {0}", contentType.ParentContentTypeName));
}

It utilizes SetContentTypeFormUrls, SetContentTypeProperties, SetFieldRefProperties and SetContentTypeId (you can read about the last method in detail in my other post – How to set Id to just created SPContentType).

public static void SetContentTypeFormUrls(SPContentType spContentType, ContentTypeFormUrls contentTypeFormUrls)
{
    if (contentTypeFormUrls == null)
        return;

    if (contentTypeFormUrls.NewFormUrl != null)
        spContentType.NewFormUrl = contentTypeFormUrls.NewFormUrl;

    if (contentTypeFormUrls.DisplayFormUrl != null)
        spContentType.DisplayFormUrl = contentTypeFormUrls.DisplayFormUrl;

    if (contentTypeFormUrls.EditFormUrl != null)
        spContentType.EditFormUrl = contentTypeFormUrls.EditFormUrl;
}

public static void SetContentTypeProperties(SPContentType spContentType, ContentTypeProperties cntProps)
{
    if (cntProps == null)
        return;

    if (cntProps.Description != null)
        spContentType.Description = cntProps.Description;
    if (cntProps.Group != null)
        spContentType.Group = cntProps.Group;
    if (cntProps.IsHidden != null)
        spContentType.Hidden = cntProps.IsHidden.Value;
    if (cntProps.IsReadOnly != null)
        spContentType.ReadOnly = cntProps.IsReadOnly.Value;
    if (cntProps.IsSealed != null)
        spContentType.Sealed = cntProps.IsSealed.Value;
    if (cntProps.NewDocumentControl != null)
        spContentType.NewDocumentControl = cntProps.NewDocumentControl;
    if (cntProps.RequireClientRenderingOnNew != null)
        spContentType.RequireClientRenderingOnNew = cntProps.RequireClientRenderingOnNew.Value;
}

public static void SetContentTypeId(SPContentType spContentType, SPContentTypeId contentTypeId)
{
    try
    {
        FieldInfo fi = typeof(SPContentType).GetField("m_id", BindingFlags.NonPublic | BindingFlags.Instance);
        if (fi != null)
            fi.SetValue(spContentType, contentTypeId);
        else
            Console.Write("Couldn't set content type id!");
    }
    catch(Exception ex)
    {
        Console.Write(string.Format("Couldn't set content type id! {0}", ex.Message));
    }
}

public static void SetFieldRefProperties(SPFieldLink fLink, FieldRef fieldRef)
{
    if (fieldRef == null)
        return;

    if (fieldRef.DisplayName != null)
        fLink.DisplayName = fieldRef.DisplayName;
    if (fieldRef.IsHidden != null)
        fLink.Hidden = fieldRef.IsHidden.Value;
    if (fieldRef.IsRequired != null)
        fLink.Required = fieldRef.IsRequired.Value;
    if (fieldRef.IsReadOnly != null)
        fLink.ReadOnly = fieldRef.IsReadOnly.Value;

    if (fieldRef.Aggregation != null)
        fLink.AggregationFunction = fieldRef.Aggregation;
    if (fieldRef.Customization != null)
        fLink.Customization = fieldRef.Customization;
    if (fieldRef.PIAttribute != null)
        fLink.PIAttribute = fieldRef.PIAttribute;
    if (fieldRef.PITarget != null)
        fLink.PITarget = fieldRef.PITarget;
    if (fieldRef.PrimaryPIAttribute != null)
        fLink.PrimaryPIAttribute = fieldRef.PrimaryPIAttribute;
    if (fieldRef.PrimaryPITarget != null)
        fLink.PrimaryPITarget = fieldRef.PrimaryPITarget;
    if (fieldRef.Node != null)
        fLink.XPath = fieldRef.Node;
}

Here is how you can use all of that stuff:

public void AddSomeContentType(SPWeb spWeb, SPList spList)
{
    ContentType newContentType = new ContentType()
    {
        ParentContentTypeName = "Item",

        Id   = new SPContentTypeId("0x0100078C8B39671A4532AB9C5AB6DCB388A6"), // id has to correspond to the parent content type
        Name = "some new content type name",

        Properties = new ContentTypeProperties() { Description = "super modern content type", Group = "List Content Types" },
        FormUrls   = new ContentTypeFormUrls() { DisplayFormUrl = "Lists/your list name/your custom page name.aspx" },
        FieldRefs  = new List<FieldRef>() 
            { 
                new FieldRef() { Name = "Title",  ID = new Guid("{fa564e0f-0b71-4ab7-b863-0177e6ddd247}"), IsRequired = false, IsReadOnly = true, DisplayName = "List Item Title" },
                new FieldRef() { Name = "Status", IsRequired = true, DisplayName = "List Item Status" },
                new FieldRef() { Name = "Involved_User", ID = new Guid("869963ef-9ca3-4ad7-a5f0-8fff724a6877"), DisplayName = "User Involved" } 
            }
    };

    AddContentType(spWeb, spList, newContentType);
}

Please note that you don’t have to fill out all properties of the auxiliary classes (ContentTypeProperties, ContentTypeFormUrls and FieldRef); the unspecified properties will be merely ignored, and the appropriate properties of SPContentType object will be left with their default values. Als,o pay attention to the fact that Content Type identifier has to contain the id of parent Content Type. Find out how to build a Content Type identifier recursively here.

SharePoint: How to set Id to just created SPContentType

September 15th, 2011 No comments

     When adding Content Type programmatically, sometimes you may need to set a certain Id to it, to ensure that other parts of the application can refer to the Content Type using a known identifier. If you use SharePoint 2010 you can get this done instantly, as SharePoint 2010 provides a handy SPContentType constructor, which accepts SPContentTypeId as parameter:

public SPContentType(SPContentTypeId contentTypeId, SPContentTypeCollection contentTypes, string name);

But if you use SharePoint 2007, you don’t have such a constructor or any built-in means to set the required identifier. Besides, the Id property of SPContentType appears to be read-only. After studying the SPContentType class with Reflector I’ve discovered that the Content Type id is stored in the private m_id property:

private SPContentTypeId m_id;

This means that we can use Reflection to set this property. Here is a special SetContentTypeId method I’ve implemented for doing that:

public void SetContentTypeId(SPContentType spContentType, SPContentTypeId contentTypeId)
{
    try
    {
        FieldInfo fi = typeof(SPContentType).GetField("m_id", BindingFlags.NonPublic | BindingFlags.Instance);                
        fi.SetValue(spContentType, contentTypeId);                
    }
    catch (Exception ex)
    {
        Console.Write(string.Format("Couldn't set content type id! {0}", ex.Message));
    }
}

The method accepts the instance of the SPContentType class and the required identifier as the instance of the SPContentTypeId class. Here is an example of use:

public void AddSomeNewContentType(SPWeb spWeb, SPList spList)
{
    SPContentType parentContentType = spWeb.AvailableContentTypes["some parent content type name"];
    if (parentContentType != null)
    {
        SPContentType spContentType = new SPContentType(parentContentType, spList.ContentTypes, "some new content type name");
        SetContentTypeId(spContentType, new SPContentTypeId("0x0100078C8B39671A4532AB9C5AB6DCB388A6")); // content type id you need has to be here, for example 0x0100078C8B39671A4532AB9C5AB6DCB388A6

        // set other properties of content type

        spList.ContentTypes.Add(spContentType);
        spContentType.Update();
        spList.Update();
    }
}

Please note that you must NOT use the SetContentTypeId method with existing built-in or earlier created Content Types. Otherwise, it may corrupt the SharePoint data integrity, especially when there are list items created based on the changed Content Type.

Note again, use the SetContentTypeId method ONLY immediately following the creation of a Content Type and ONLY before adding it to whatever collection of Content Types or before calling SPContentType.Update(). This is very important.

My next post describes how to add Content Type programmatically.