Archive

Posts Tagged ‘Business Data Catalog’

SharePoint: Working with BDC Secondary Fields

April 17th, 2012 No comments

    As you probably know, in SharePoint 2010 Business Data Connectivity replaced Business Data Catalog of SharePoint 2007. Some changes affects how Business Data Columns are presented in a list’s schema. In SP 2007 a declaration of a Business Data Column in a schema.xml may look like the following:

<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"
RelatedField="Products_ID"
RelatedFieldBDCField="" RelatedFieldWssStaticName="Products_ID"

SecondaryFieldBdcNames="Price:Producer"
SecondaryFieldWssNames="Product_x003a__x0020_Price:Product_x003a__x0020_Producer"
SecondaryFieldsWssStaticNames="Product_x003a__x0020_Price:Product_x003a__x0020_Producer" />

In contrast, in SP 2010 it looks like

<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"
RelatedField="Products_ID"
RelatedFieldBDCField="" RelatedFieldWssStaticName="Products_ID"

SecondaryFieldBdcNames="6%209%20Price%20Producer%204"
SecondaryFieldWssNames="27%2030%20Product%5Fx003a%5F%5Fx0020%5FPrice%20Product%5Fx003a%5F%5Fx0020%5FProducer%206"
SecondaryFieldsWssStaticNames="27%2030%20Product%5Fx003a%5F%5Fx0020%5FPrice%20Product%5Fx003a%5F%5Fx0020%5FProducer%206" />

Undoubtedly, in SP 2010 the secondary fields became practically unreadable. Indeed, the format of secondary fields‘ presentation is revised. Moreover some kind of URL encoding are applied to them. Let’s examine how these secondary fields could look before the URL encoding is applied:

<Field
...
SecondaryFieldBdcNames="6 9 Price Producer 4"
SecondaryFieldWssNames="27 30 Product_x003a__x0020_Price Product_x003a__x0020_Producer 6"
SecondaryFieldsWssStaticNames="27 30 Product_x003a__x0020_Price Product_x003a__x0020_Producer 6" />

Now it’s pretty easy to figure out the new format. Take a look at the SecondaryFieldBdcNames attribute. It contains names of two secondary bdc fields: ‘Price’ and ‘Producer’. 6 is the length of the ‘Price’ name + 1 for a space character right after the name. 9 is the length of the ‘Procuder’ name + 1 for a space character after the name. 4 is the length of the sub-string ‘6 9 ‘ (including spaces), which contains the lengths of the fields’ names. See a picture below:

Format of Secondary Fields

Note that the SecondaryFieldBdcNames, SecondaryFieldWssNames and SecondaryFieldsWssStaticNames have the same format.

We have a lot of code interacting with Business Data Columns, thus we were interested in means allowing easily to decode, encode and parse Secondary Fields attributes. In the Microsoft.SharePoint.dll, there is the internal BdcClientUtil class containing the basic methods to work with Secondary Fields:

internal class BdcClientUtil
{
    ...
    string[] SplitStrings(string combinedEncoded);
    string   CombineStrings(string[] strings);
    ...
}

So, using .Net Reflector I’ve extracted these methods along with several others auxiliary ones and put them into the helper-class called SecondaryFieldNamesHelper. All internal methods and properties were honestly stolen from Microsoft.SharePoint.dll, the public ones were added by me and described below:

  • string Encode(string[] secondaryFieldNames) – accepts an array of field names and returns the string formatted and encoded according to the SharePoint 2010 requirements;
  • string[] Decode(string str) – accepts an encoded string, decodes it and returns a resultant array of field names;
  • bool IsEncodedString(string str) – checks whether a passed string is encoded;
  • string ConvertToSP2010(string str) – converts a SP 2007 colon-separated string of secondary fields into another one formatted and encoded according to the SharePoint 2010 requirements;

Below is the source code of the SecondaryFieldNamesHelper:

SecondaryFieldNamesHelper Sources

using System;
using System.Collections;
using System.Text;
using System.Globalization;
using System.IO;
using System.Web.UI;

namespace Helpers
{
    public static class SecondaryFieldNamesHelper
    {
        #region fields & properties
        private static string[] s_crgstrUrlHexValue = new string[] 
        { 
            "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", "%08", "%09", "%0A", "%0B", "%0C", "%0D", "%0E", "%0F", 
            "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17", "%18", "%19", "%1A", "%1B", "%1C", "%1D", "%1E", "%1F", 
            "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27", "%28", "%29", "%2A", "%2B", "%2C", "%2D", "%2E", "%2F", 
            "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37", "%38", "%39", "%3A", "%3B", "%3C", "%3D", "%3E", "%3F", 
            "%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47", "%48", "%49", "%4A", "%4B", "%4C", "%4D", "%4E", "%4F", 
            "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57", "%58", "%59", "%5A", "%5B", "%5C", "%5D", "%5E", "%5F", 
            "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67", "%68", "%69", "%6A", "%6B", "%6C", "%6D", "%6E", "%6F", 
            "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77", "%78", "%79", "%7A", "%7B", "%7C", "%7D", "%7E", "%7F", 
            "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87", "%88", "%89", "%8A", "%8B", "%8C", "%8D", "%8E", "%8F", 
            "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97", "%98", "%99", "%9A", "%9B", "%9C", "%9D", "%9E", "%9F", 
            "%A0", "%A1", "%A2", "%A3", "%A4", "%A5", "%A6", "%A7", "%A8", "%A9", "%AA", "%AB", "%AC", "%AD", "%AE", "%AF", 
            "%B0", "%B1", "%B2", "%B3", "%B4", "%B5", "%B6", "%B7", "%B8", "%B9", "%BA", "%BB", "%BC", "%BD", "%BE", "%BF", 
            "%C0", "%C1", "%C2", "%C3", "%C4", "%C5", "%C6", "%C7", "%C8", "%C9", "%CA", "%CB", "%CC", "%CD", "%CE", "%CF", 
            "%D0", "%D1", "%D2", "%D3", "%D4", "%D5", "%D6", "%D7", "%D8", "%D9", "%DA", "%DB", "%DC", "%DD", "%DE", "%DF", 
            "%E0", "%E1", "%E2", "%E3", "%E4", "%E5", "%E6", "%E7", "%E8", "%E9", "%EA", "%EB", "%EC", "%ED", "%EE", "%EF", 
            "%F0", "%F1", "%F2", "%F3", "%F4", "%F5", "%F6", "%F7", "%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF"
        };
        #endregion

        #region public methods
        public static bool IsEncodedString(string str)
        {
            if (string.IsNullOrEmpty(str))
                return false;

            bool res = true;
            try
            {
                string[] splittedString = SplitStrings(str);
            }
            catch
            {
                res = false;
            }
            return res;
        }

        public static string Encode(string[] secondaryFieldNames)
        {
            return CombineStrings(secondaryFieldNames);
        }

        public static string[] Decode(string str)
        {
            if(string.IsNullOrEmpty(str))
                return new string[0];
            return SplitStrings(str);
        }

        public static string ConvertToSP2010(string str)
        {
            if (IsEncodedString(str))
                return str;

            string[] fieldNames = str.Split(new string[] { ":" }, StringSplitOptions.RemoveEmptyEntries);
            string encodedVal = CombineStrings(fieldNames);
            return encodedVal;
        }
        #endregion

        #region internal methods
        private static string[] SplitStrings(string combinedEncoded)
        {
            string[] array = null;
            ArrayList list = new ArrayList();
            if ("0" == combinedEncoded)
                return new string[0];
            try
            {
                string str = UrlKeyValueDecode(combinedEncoded);
                string[] strArray2 = str.Split(new char[] { ' ' }, StringSplitOptions.None);
                int result = 0;
                if ((strArray2 == null) || !int.TryParse(strArray2[strArray2.Length - 1], NumberStyles.Integer, 

CultureInfo.InvariantCulture, out result))
                    throw new ArgumentException(string.Empty, "combinedEncoded");
                int num2 = str.LastIndexOf(' ');
                string str2 = str.Substring(result, num2 - result);
                int length = str2.Length;
                int index = 0;
                int startIndex = 0;
                while (startIndex < length)
                {
                    string s = strArray2[index];
                    int num6 = 1;
                    if ((s != null) && (s.Length == 0))
                        list.Add(null);
                    else
                    {
                        if (!int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out num6))
                            throw new ArgumentException(string.Empty, "combinedEncoded");
                        list.Add(str2.Substring(startIndex, num6 - 1));
                    }
                    startIndex += num6;
                    index++;
                }
                array = new string[list.Count];
                list.CopyTo(array);
            }
            catch (Exception exception)
            {
                throw new ArgumentException(string.Empty, "combinedEncoded", exception);
            }
            return array;
        }

        private static string UrlKeyValueDecode(string keyOrValueToDecode)
        {
            if (string.IsNullOrEmpty(keyOrValueToDecode))
                return keyOrValueToDecode;
            return UrlDecodeHelper(keyOrValueToDecode, keyOrValueToDecode.Length, true);
        }

        private static string UrlDecodeHelper(string stringToDecode, int length, bool decodePlus)
        {
            if ((stringToDecode == null) || (stringToDecode.Length == 0))
                return stringToDecode;
            StringBuilder builder = new StringBuilder(length);
            byte[] bytes = null;
            int nIndex = 0;
            while (nIndex < length)
            {
                char ch = stringToDecode[nIndex];
                if (ch < ' ')
                    nIndex++;
                else
                {
                    if (decodePlus && (ch == '+'))
                    {
                        builder.Append(" ");
                        nIndex++;
                        continue;
                    }
                    if (IsHexEscapedChar(stringToDecode, nIndex, length))
                    {
                        if (bytes == null)
                            bytes = new byte[(length - nIndex) / 3];
                        int count = 0;
                        do
                        {
                            int num3 = (FromHexNoCheck(stringToDecode[nIndex + 1]) * 0x10) + FromHexNoCheck(stringToDecode[nIndex + 

2]);
                            bytes[count++] = (byte)num3;
                            nIndex += 3;
                        }
                        while (IsHexEscapedChar(stringToDecode, nIndex, length));
                        builder.Append(Encoding.UTF8.GetChars(bytes, 0, count));
                        continue;
                    }
                    builder.Append(ch);
                    nIndex++;
                }
            }
            if (length < stringToDecode.Length)
                builder.Append(stringToDecode.Substring(length));
            return builder.ToString();
        }

        private static bool IsHexEscapedChar(string str, int nIndex, int nPathLength)
        {
            if ((((nIndex + 2) >= nPathLength) || (str[nIndex] != '%')) || (!IsHexDigit(str[nIndex + 1]) || !IsHexDigit(str[nIndex + 

2])))
                return false;
            if (str[nIndex + 1] == '0')
                return (str[nIndex + 2] != '0');
            return true;
        }

        private static bool IsHexDigit(char digit)
        {
            if ((('0' > digit) || (digit > '9')) && (('a' > digit) || (digit > 'f')))
                return (('A' <= digit) && (digit <= 'F'));
            return true;
        }

        private static int FromHexNoCheck(char digit)
        {
            if (digit <= '9')
                return (digit - '0');
            if (digit <= 'F')
                return ((digit - 'A') + 10);
            return ((digit - 'a') + 10);
        }

        private static string CombineStrings(string[] strings)
        {
            StringBuilder builder = new StringBuilder();
            int index = 0;
            for (int i = 0; i < strings.Length; i++)
            {
                string str = strings[i];
                string str2 = ((str != null) ? ((str.Length + 1)).ToString(CultureInfo.InvariantCulture) : string.Empty) + ' ';
                builder.Insert(index, str2);
                index += str2.Length;
                builder.Append(str + ' ');
            }
            builder.Append(index.ToString(CultureInfo.InvariantCulture));
            return UrlKeyValueEncode(builder.ToString());
        }

        private static string UrlKeyValueEncode(string keyOrValueToEncode)
        {
            if ((keyOrValueToEncode == null) || (keyOrValueToEncode.Length == 0))
                return keyOrValueToEncode;
            StringBuilder sb = new StringBuilder(0xff);
            HtmlTextWriter output = new HtmlTextWriter(new StringWriter(sb, CultureInfo.InvariantCulture));
            UrlKeyValueEncode(keyOrValueToEncode, output);
            return sb.ToString();
        }

        private static void UrlKeyValueEncode(string keyOrValueToEncode, TextWriter output)
        {
            if (((keyOrValueToEncode != null) && (keyOrValueToEncode.Length != 0)) && (output != null))
            {
                bool fUsedNextChar = false;
                int startIndex = 0;
                int length = 0;
                int num3 = keyOrValueToEncode.Length;
                for (int i = 0; i < num3; i++)
                {
                    char ch = keyOrValueToEncode[i];
                    if (((('0' <= ch) && (ch <= '9')) || (('a' <= ch) && (ch <= 'z'))) || (('A' <= ch) && (ch <= 'Z')))
                        length++;
                    else
                    {
                        if (length > 0)
                        {
                            output.Write(keyOrValueToEncode.Substring(startIndex, length));
                            length = 0;
                        }
                        UrlEncodeUnicodeChar(output, keyOrValueToEncode[i], (i < (num3 - 1)) ? keyOrValueToEncode[i + 1] : '\0', out 

fUsedNextChar);
                        if (fUsedNextChar)
                            i++;
                        startIndex = i + 1;
                    }
                }
                if ((startIndex < num3) && (output != null))
                    output.Write(keyOrValueToEncode.Substring(startIndex));
            }
        }

        private static void UrlEncodeUnicodeChar(TextWriter output, char ch, char chNext, out bool fUsedNextChar)
        {
            bool fInvalidUnicode = false;
            UrlEncodeUnicodeChar(output, ch, chNext, ref fInvalidUnicode, out fUsedNextChar);
        }

        private static void UrlEncodeUnicodeChar(TextWriter output, char ch, char chNext, ref bool fInvalidUnicode, out bool 

fUsedNextChar)
        {
            int num = 0xc0;
            int num2 = 0xe0;
            int num3 = 240;
            int num4 = 0x80;
            int num5 = 0xd800;
            int num6 = 0xfc00;
            int num7 = 0x10000;
            fUsedNextChar = false;
            int index = ch;
            if (index <= 0x7f)
                output.Write(s_crgstrUrlHexValue[index]);
            else
            {
                int num8;
                if (index <= 0x7ff)
                {
                    num8 = num | (index >> 6);
                    output.Write(s_crgstrUrlHexValue[num8]);
                    num8 = num4 | (index & 0x3f);
                    output.Write(s_crgstrUrlHexValue[num8]);
                }
                else if ((index & num6) != num5)
                {
                    num8 = num2 | (index >> 12);
                    output.Write(s_crgstrUrlHexValue[num8]);
                    num8 = num4 | ((index & 0xfc0) >> 6);
                    output.Write(s_crgstrUrlHexValue[num8]);
                    num8 = num4 | (index & 0x3f);
                    output.Write(s_crgstrUrlHexValue[num8]);
                }
                else if (chNext != '\0')
                {
                    index = (index & 0x3ff) << 10;
                    fUsedNextChar = true;
                    index |= chNext & 'Ͽ';
                    index += num7;
                    num8 = num3 | (index >> 0x12);
                    output.Write(s_crgstrUrlHexValue[num8]);
                    num8 = num4 | ((index & 0x3f000) >> 12);
                    output.Write(s_crgstrUrlHexValue[num8]);
                    num8 = num4 | ((index & 0xfc0) >> 6);
                    output.Write(s_crgstrUrlHexValue[num8]);
                    num8 = num4 | (index & 0x3f);
                    output.Write(s_crgstrUrlHexValue[num8]);
                }
                else
                    fInvalidUnicode = true;
            }
        }
        #endregion
    }
}

The SecondaryFieldNamesHelper can be used as shown below:

SPBusinessDataField bdcField = ...

string secondaryFieldWssNamesProperty = bdcField.GetProperty("SecondaryFieldWssNames");
string[] secondaryWssFieldNames = SecondaryFieldNamesHelper.Decode(property);

string secondaryFieldBdcNamesProperty = bdcField.GetProperty("SecondaryFieldBdcNames");
string[] secondaryFieldBdcNames = SecondaryFieldNamesHelper.Decode(secondaryFieldBdcNamesProperty);

string sp2010WssStaticNames = 
   SecondaryFieldNamesHelper.ConvertToSP2010("Product_x003a__x0020_Price:Product_x003a__x0020_Producer");

As a .cs file the SecondaryFieldNamesHelper class is available here.

SharePoint: Manually Upgrade Business Data Catalog Application Definitions to Business Data Connectivity Models

March 13th, 2012 No comments

    Trying to import a legacy Application Definition File of SharePoint 2007 into Business Data Connectivity Service of SharePoint 2010, you apparently got at least one of the errors shown below:

  • Application definition import failed. The following error occurred: The root element of a valid Metadata package must be ‘Model’ in namespace ‘http://schemas.microsoft.com/windows/2007/BusinessDataCatalog’. The root in the given package is ‘LobSystem’ in namespace ‘http://schemas.microsoft.com/office/2006/03/BusinessDataCatalog’. Error was encountered at or just before Line: ‘2’ and Position: ‘2’;
  • Application definition import failed. The following error occurred: BDC Model does not correctly match the schema. The required attribute ‘Namespace’ is missing. Error was encountered at or just before Line: ’20’ and Position: ’10’;
  • Application definition import failed. The following error occurred: ReturnTypeDescriptor of MethodInstance with Name ‘ProductSpecificFinderInstance’ on Entity (External Content Type) with Name ‘Product’ in Namespace ‘ExternalProductDB’ should not be a Collection TypeDescriptor for MethodInstances of Type ‘SpecificFinder’. Parameter name: rawValues.ReturnTypeDescriptorId Error was encountered at or just before Line: ‘171’ and Position: ’18’;
  • and so on

As it’s known, in SharePoint 2010, Business Data Catalog (BDC) was replaced with Business Data Connectivity with the same abbreviation. One of the changed things is the format of xml-based Application Definition Files. If you make an in-place upgrade of a live SharePoint 2007 application to SharePoint 2010, bdc metadata will be automatically upgraded as well and will become usable with the Business Data Connectivity. But if the in-place upgrade isn’t an option for you, you can upgrade your xml-based Application Definition Files manually. The manual algorithm step by step is described here – How to: Manually Upgrade Business Data Catalog Application Definitions to Business Data Connectivity Models.

For one of our applications we settled on the manual upgrade of its metadata files. But If I call myself a programmer, I have to try to automate the algorithm, especially taking into account 20+ files required to upgrade. So, I’ve developed a simple application for alteration of the legacy xml-based Application Definition Files to make them compatible with SharePoint 2010. However I’d like to notice that the given converter doesn’t follow entirely the procedure described by Microsoft, but performs only steps allowing our particular metadata files to be successfully imported into the Business Data Connectivity Service. For example, our files don’t comprise actions and associations, thus the application does nothing at all with <Action> and <Association> elements. So, consider this converter as a start point of developing the new one satisfying your own conditions and requirements.

Below I enumerated the necessary and sufficient changes to be applied to our particular metadata files so that it enables us to make them compatible with SharePoint 2010. Exactly these very steps and a few less important I’ve implemented in the converter.

  • the root element in the Application Definition File must be a <Model>;
  • the <Model> must contain <LobSystems> element, which in turn must wrap the former root node – <LobSystem>;
  • the <LobSystem> element mustn’t contain the Version-attribute;
  • the <Entity> element must contain the new attributes – Namespace and Version;
  • the <Identifier> element mustn’t contain an assembly name in its TypeName-attribute; For example, TypeName=”System.String, mscorlib” has to turn into TypeName=”System.String”;
  • the <MethodInstance> element with Type-attribute value of SpecificFinder should include the Default-attribute with value of true;
  • if the <TypeDescriptor> element has the IsCollection attribute set to true, the MethodInstance return TypeDescriptor should be updated to be an element of the collection. In practice, that means the ReturnTypeDescriptorPath-attribute with an appropriate value should be added to <MethodInstance> element, and the obsolete ReturnTypeDescriptorLevel and ReturnTypeDescriptorName attributes should be deleted;

Note that when modifying a <Entity> element, the values of the Namespace and Version attributes are copied respectively from the values of Name and Version attributes of the <LobSystem> element, which wraps the <Entity> element. The same approach is used while the in-place upgrade takes place.

After the changes are applied, and if they are sufficient for your metadata, the result files can be imported into Business Data Connectivity Service. During the import process, you may get the warnings. Consider fixing them in the future, but at the present stage you can simply ignore them. The most popular warnings are listed below:

  • This Model contains LobSystem (External System) of Type ‘WebService’ which is deprecated and may be removed in future releases.
  • The MethodInstance of type ‘Finder’ with Name ‘FindProducts’ does not have a Limit Filter.
  • The TypeDescriptor ‘From’ on Parameter ‘GetProductsFiltered’ on Method ‘GetItems’ of Entity (External Content Type) ‘Product’ with Namespace ‘ExternalProductDB’ has a DateTime value, but there is no Interpretation describing what the External System expects and returns as the DateTimeKind. The DateTime value is assumed to be UTC.
  • Note: I made the converter-application fix the warnings regarding the DateTime type and UTC, so they won’t bother you.

    The converter is very straightforward to use. Using the button ‘+’, simply add to the left section the files to be upgraded. Then click the button ‘>>’, and you’ll get the upgraded ones in the right section. Double click on file name opens an overview form to browse the input or result xml. Physically, the result files are located in c:\output folder. The application doesn’t use any SharePoint-related libraries.

    Upgrade 2007 BDC model to SP2010

    You can download the application from this page or by using the direct link. The Visual Studio 2010 solution and appropriate executable file are in the archive.

SharePoint: Enhanced ItemPicker

February 28th, 2012 No comments

    I was asked to develop a control based on the ItemPicker control, which, in addition to ability of choosing an external data item through BDC, brings it to client side without page postback. There was the following supposed sequence of actions:

  1. An user chooses a data item in the Picker Dialog;
  2. The identifier of the selected item is sent to the server through an Ajax-like technology;
  3. Using the received identifier, the server fetches the proper data out through BDC and sends it back to the client. By “the proper data” I mean the values of either all fields available in the selected data item or only fields defined in control declaration;
  4. On the client side, the received data is parsed and displayed in UI;

Additionally, the control should be free of bindings to SPField as it should be capable to reside within an independent aspx-page locating in the _layout folder.

*Note: for better understanding of BDC infrastructure, please read the following blog posts: SharePoint: Brief introduction to Business Data Catalog and SharePoint: Understanding BusinessData Column.

So, I developed the required control and made it as reusable as possible. Let’s call the control MyItemPicker (it’s so unusual, isn’t? :)). For sake of simplicity I decided to use the ASP.Net client callbacks applied through the ICallbackEventHandler interface. The ASP.Net client callbacks can be considered as a wrapper on XMLHTTP object. Also the MyItemPicker comprises and uses the standard ItemPicker.

Ok, let’s start with declaration of the control within page:

<MYCC:MyItemPicker id="myItemPicker" runat="server" LobSystemInstanceName="Products" 
EntityName="Product" PrimaryColumnName="Name" ClientCallback="MyClientCallback" 
ClientCallbackError="MyClientCallbackError" CallbackBDCFieldFilter="Price,Producer" />

The significant properties here are

  • LobSystemInstanceName is the name of the Lob System Instance, through which data is provided to pick;
  • EntityName is the type name of data items populating the picker;
  • PrimaryColumnName is the name of the data item field, the value of which is used as a display value;
  • ClientCallback is the name of the JavaScript function, which has to be present within the page. In case of success, the given function accepts and processes the server response containing fetched data;
  • ClientCallbackError is the name of the JavaScript function, which can be within the page and is called, when server fails to fulfill request. This property is optional;
  • CallbackBDCFieldFilter is the comma-separated string containing names of data item fields that should be included in server response. For example, if a BDC Entity has four fields – ID, Name, Price and Producer, you might want to have on client side only two of them – Price and Producer. If the CallbackBDCFieldFilter property is empty or not presented in the declaration, server response contains the values of all available fields of BDC Entity;

The sample of the JavaScript functions, which should be indicated in the ClientCallback and ClientCallbackError properties, is shown below. Note the functions’ signatures.

<script type="text/javascript">

    function MyClientCallback(result, context) {

        alert("Result: " + result);
        
        if (result != null && typeof result != "undefined" && result.length != 0) {
            var res = eval("(" + result + ")");

            alert('Price: '    + res.Price);
            alert('Producer: ' + res.Producer);
            
            // update UI with received data
        }        
    }

    function MyClientCallbackError(result, context) {
        alert('Error: ' + result);            
    }

</script>

The server response looks like

{ 'Price' : '10.00', 
  'Producer' : 'Microsoft Corporation' }

Thus, the response is formatted in the manner to be easily turned into JavaScript object by means of the eval function.

So, it’s time to examine the source code of the MyItemPicker itself.

MyItemPicker Source

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

namespace MyControls
{
    public class MyItemPicker : Control, ICallbackEventHandler
    {
        #region fields & properties
        protected ItemPicker _picker                    = null;
        protected string     _callbackRequestedEntityId = string.Empty;

        public string LobSystemInstanceName  { get; set; }
        public string EntityName             { get; set; }
        public string PrimaryColumnName      { get; set; }

        public string ClientCallback         { get; set; }
        public string ClientCallbackError    { get; set; }

        public string CallbackBDCFieldFilter { get; set; }
        #endregion

        #region public methods
        // Implementation of the ICallbackEventHandler interface
        // Generates response that will be sent to client
        public string GetCallbackResult()
        {
            return GetJSResult();
        }
        // Implementation of the ICallbackEventHandler interface
        // Retrieves and preserves identifier of selected data item sent from client
        public void RaiseCallbackEvent(string eventArgument)
        {
            _callbackRequestedEntityId = eventArgument;
        }
        #endregion

        #region internal methods
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            EnsureChildControls();            
        }

        protected override void CreateChildControls()
        {            
            base.CreateChildControls();

            if (_picker == null)
            {
                _picker = new ItemPicker();                
                _picker.MultiSelect = false;
                _picker.ID = ID + "_ItemPicker";
                try
                {
                    this.SetExtendedDataOnPicker(_picker);
                }
                catch (Exception exception)
                {
                    _picker.ErrorMessage = exception.Message;
                    _picker.Enabled = false;
                }

                this.Controls.Add(_picker);
            }            
        }

	/// <summary>
        /// Initilizes main item picker's properties
        /// </summary>        
        protected virtual void SetExtendedDataOnPicker(ItemPicker picker)
        {            
            ItemPickerExtendedData data = new ItemPickerExtendedData();

            BDCMetaRequest request = new BDCMetaRequest(LobSystemInstanceName, EntityName);
            data.SystemInstanceId  = request.FoundLobSystemInstance.Id;
            data.EntityId          = request.FoundEntity.Id;

            List<uint> list = new List<uint>();
            FieldCollection fields = request.FoundEntity.GetSpecificFinderView().Fields;
            foreach (Field field in fields)
                if (string.Equals(field.Name, PrimaryColumnName, StringComparison.OrdinalIgnoreCase))                
                    data.PrimaryColumnId = field.TypeDescriptor.Id;
                else
                    list.Add(field.TypeDescriptor.Id);

            data.SecondaryColumnsIds = list.ToArray();
            picker.ExtendedData = data;
        }

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            AddJSCallbackFunctions();
            AddAdditionalJSFunctions();
        }

        /// <summary>
        /// Generates and adds auxiliary JavaScript functions to the page
        /// </summary> 
        protected void AddAdditionalJSFunctions()
        {
            if (_picker != null)
            {
                _picker.LoadPostData(null, null); // this line is required to force CreateChildControls() and to have HiddenEntityKey created
                Control upLevelDiv = FindControlRecursive(_picker, "upLevelDiv");
                if (upLevelDiv != null)
                {
                    string clearFuncName = "ClearItemPicker_" + ID;
                    string clearFunc =
                        "function " + clearFuncName + "() {" +
                            "var upLevelDiv = document.getElementById('" + upLevelDiv.ClientID + "');" +
                            "if (upLevelDiv != null) {" +
                                "upLevelDiv.innerHTML = '';" +
                                "updateControlValue('" + _picker.ClientID + "');" +
                                "}" +
                            "}";
                    Page.ClientScript.RegisterClientScriptBlock(GetType(), clearFuncName, clearFunc, true);
                }
                
                Control hiddenEntityDisplayTextControl = FindControlRecursive(_picker, "HiddenEntityDisplayText");
                if (hiddenEntityDisplayTextControl != null)
                {
                    string getDisplayTextFuncName = "GetDisplayText_" + ID;
                    string getDisplayTextFunc =
                        "function " + getDisplayTextFuncName + "() {" +
                            "var hiddenEntityDisplayTextControl = document.getElementById('" + hiddenEntityDisplayTextControl.ClientID + "');" +
                            "return hiddenEntityDisplayTextControl != null ? hiddenEntityDisplayTextControl.value : '';" +
                        "}";
                    Page.ClientScript.RegisterClientScriptBlock(GetType(), getDisplayTextFuncName, getDisplayTextFunc, true);
                }
            }            
        }

        /// <summary>
        /// Generates and adds the picker's AfterCallbackClientScript to the page
        /// </summary> 
        protected void AddJSCallbackFunctions()
        {
            if (_picker != null)
            {
                string callbackFunc = null;
                if (!string.IsNullOrEmpty(ClientCallback) && !string.IsNullOrEmpty(ClientCallbackError))
                    callbackFunc = Page.ClientScript.GetCallbackEventReference(this, "arg", ClientCallback, "context", ClientCallbackError, true);
                else
                {
                    if (!string.IsNullOrEmpty(ClientCallback))
                        callbackFunc = Page.ClientScript.GetCallbackEventReference(this, "arg", ClientCallback, "context", true);
                }
                if (!string.IsNullOrEmpty(callbackFunc))
                {
                    _picker.LoadPostData(null, null); // this line is required to force CreateChildControls() and to have HiddenEntityKey created

                    Control pickerEntityKeyHidden = FindControlRecursive(_picker, "HiddenEntityKey");
                    if (pickerEntityKeyHidden != null)
                    {
                        string clientFuncName = "GetBdcFieldValuesAsync_" + ID;
                        string clientFunc =
                            "function " + clientFuncName + "(context)" +
                            "{" +
                                "var pickerEntityKeyHidden = document.getElementById('" + pickerEntityKeyHidden.ClientID + "');" +
                                "if (pickerEntityKeyHidden != null)" +
                                "{" +
                                    "var arg = pickerEntityKeyHidden.value;" +
                                    callbackFunc + ";" +
                                "}" +
                            "}";
                        Page.ClientScript.RegisterClientScriptBlock(GetType(), clientFuncName, clientFunc, true);
                        _picker.AfterCallbackClientScript = clientFuncName + "('" + ID + "');";
                    }
                }
            }
        }

        /// <summary>
        /// Makes request to external data source and returns json-result
        /// </summary>         
        protected string GetJSResult()
        {
            string res = string.Empty;

            try
            {
                if (!string.IsNullOrEmpty(_callbackRequestedEntityId))
                {
                    Dictionary<string, byte> bdcFieldFilter = GetBDCFieldFilter(CallbackBDCFieldFilter);
                    
                    BDCRequestById request = new BDCRequestById(LobSystemInstanceName, EntityName, _callbackRequestedEntityId);

                    StringBuilder sb = new StringBuilder();
                    sb.Append("{");
                    foreach (Field field in request.FoundEntityInstance.ViewDefinition.Fields)
                    {
                        if (bdcFieldFilter.Count == 0 || bdcFieldFilter.ContainsKey(field.Name))
                        {
                            if (sb.Length > 1)
                                sb.Append(", ").AppendLine();
                            sb.Append("'").Append(field.Name).Append("' : ");
                            sb.Append("'").Append(Convert.ToString(request.FoundEntityInstance.GetFormatted(field))).Append("'");
                        }
                    }
                    sb.Append("}");                    

                    res = sb.ToString();
                }
            }
            catch (Exception ex)
            {
                // write error to log
            }

            return res;
        }

        /// <summary>
        /// Parses the user defined list of bdc fields, the values of which should be retrieved
        /// </summary>        
        protected static Dictionary<string, byte> GetBDCFieldFilter(string commaSeparatedBdcFields)
        {
            Dictionary<string, byte> res = new Dictionary<string, byte>(StringComparer.OrdinalIgnoreCase);

            if (!string.IsNullOrEmpty(commaSeparatedBdcFields))
            {
                string[] bdcFields = commaSeparatedBdcFields.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string field in bdcFields)
                    res.Add(field, 0);
            }

            return res;
        }
        #endregion
    }
}

You probably noticed that the functions AddJSCallbackFunctions and AddAdditionalJSFunctions generate and add some JavaScript functions to the page. The exact names of these JavaScript functions depend on the id attribute defined in control declaration. For example, if control id is “myItemPicker“, the functions’ name will be GetBdcFieldValuesAsync_myItemPicker, ClearItemPicker_myItemPicker and GetDisplayText_myItemPicker.

Let’s take a look at the functions. The main function is GetBdcFieldValuesAsync_myItemPicker, which extracts the encoded id of selected item from ItemPicker and then makes the Ajax-like client callback to the server. The rest two functions are auxiliary, they are not used by MyItemPicker directly, but they are very useful for developing an interaction between user and MyItemPicker. As their names imply, the ClearItemPicker_myItemPicker clears the ItemPicker, and the GetDisplayText_myItemPicker returns the text displayed to user in ItemPicker. The listing below demonstrates the functions within page:

<script type="text/javascript">

    function GetBdcFieldValuesAsync_myItemPicker(context) {
        var pickerEntityKeyHidden = document.getElementById('myItemPicker_ItemPicker_HiddenEntityKey');
        if (pickerEntityKeyHidden != null) {
            var arg = pickerEntityKeyHidden.value;
            WebForm_DoCallback('myItemPicker', arg, ClientCallback, context, ClientCallbackError, true);
        }
    }

    function ClearItemPicker_myItemPicker() {
        var upLevelDiv = document.getElementById('myItemPicker_ItemPicker_upLevelDiv');
        if (upLevelDiv != null) {
            upLevelDiv.innerHTML = '';
            updateControlValue('myItemPicker_ItemPicker');
        }
    }

    function GetDisplayText_myItemPicker() {
        var hiddenEntityDisplayTextControl = document.getElementById('myItemPicker_ItemPicker_HiddenEntityDisplayText');
        return hiddenEntityDisplayTextControl != null ? hiddenEntityDisplayTextControl.value : '';
    }
</script>

The SetExtendedDataOnPicker and GetJSResult methods of MyItemPicker employ the classes BDCMetaRequest and BDCRequestById that are described in my post SharePoint: How to get value from BDC.

The FindControlRecursive method is mentioned in another my post.

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));