Archive

Archive for the ‘Object Model’ Category

SharePoint: Getting a SPList with no exceptions to be thrown

June 15th, 2012 No comments

List 'some list name' does not exist at site with URL 'some site url'.

    Getting a SPList object in code, I prefer not using an indexer of the SPWeb.Lists collection (SPListCollection) as it throws the above exception every time when the list with the specified name wasn’t found in the collection. It should be noted, however, that in SharePoint 2010, there is the TryGetList method, that has been added to SPListCollection and which returns null if the list isn’t presented in the collection. But I still use my own simple methods free of ‘not found’ exceptions and compatible with both SharePoint 2007 and 2010.

Get SPList by title

The first method returns a SPList object by specified title or null if nothing is found:

public static SPList GetListByName(SPWeb web, string listName)
{
    listName = listName.ToLower();
    foreach (SPList spList in web.Lists)
        if (spList.Title.ToLower() == listName)
            return spList;
    return null;
}

// usage
// ...
    using (SPSite spSite = new SPSite("some site url"))
        using (SPWeb spWeb = spSite.OpenWeb())
        {
            SPList spList = GetListByName(spWeb, "Products");
        }
// ...

Get SPList by url

Unfortunately, list title tends to be changed in the course of time. Unlike title, list url is unchangeable in list’s life time. So, the use of url (or its part) for list search is more reliable. The second method exactly uses url to find a list:

public static SPList GetListByUrl(SPWeb web, string url)
{
    url = url.ToLower();
    foreach (SPList spList in web.Lists)
        if (spList.RootFolder.Url.ToLower().EndsWith(url))
            return spList;
    return null;
}

// usage
// ...
    using (SPSite spSite = new SPSite("some site url"))
        using (SPWeb spWeb = spSite.OpenWeb())
        {
            SPList spList = GetListByUrl(spWeb, "Lists/Products");
        }
// ...

I hope these methods would be useful for somebody else.

Related posts:

SharePoint: Workflow + List Item Edit Form = Value cannot be null Exception

April 25th, 2012 No comments

    After migration of a SharePoint 2007 application to SharePoint 2010 we encountered an unhandled exception occurring arbitrarily when a list item is opening for editing. The exception looks as follows:

Exception Details: System.ArgumentNullException: Value cannot be null.
Parameter name: s

Source Error: 
An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below. 

Stack Trace: 

[ArgumentNullException: Value cannot be null. Parameter name: s]
   System.IO.StringReader..ctor(String s) +10151478
   Microsoft.SharePoint.Publishing.Internal.WorkflowUtilities.FlattenXmlToHashtable(String strXml) +117
   Microsoft.SharePoint.Publishing.Internal.WorkflowUtilities.DoesWorkflowCancelWhenItemEdited(String associationXml) +12
   Microsoft.SharePoint.Publishing.WebControls.ConsoleDataSource.EnsurePageNotInLockingWorkflowIfInEditMode() +207
   Microsoft.SharePoint.Publishing.WebControls.ConsoleDataSource.LoadDataSource() +199
   Microsoft.SharePoint.Publishing.WebControls.ConsoleDataSource.OnLoad(EventArgs e) +98
   Microsoft.SharePoint.Publishing.WebControls.XmlConsoleDataSource.OnLoad(EventArgs e) +201
   Microsoft.SharePoint.Publishing.WebControls.PublishingSiteActionsMenuCustomizer.OnLoad(EventArgs e) +186
   System.Web.UI.Control.LoadRecursive() +66
   System.Web.UI.Control.LoadRecursive() +191
   System.Web.UI.Control.LoadRecursive() +191
   System.Web.UI.Control.LoadRecursive() +191
   System.Web.UI.Control.LoadRecursive() +191
   System.Web.UI.Control.LoadRecursive() +191
   System.Web.UI.Control.LoadRecursive() +191
   System.Web.UI.Control.LoadRecursive() +191
   System.Web.UI.Control.LoadRecursive() +191
   System.Web.UI.Control.LoadRecursive() +191
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +2428

Having analyzed the stack trace we found out that the issue is, evidently, caused by workflow infrastructure. In the SharePoint application we have both handmade custom workflows and the ones that were made in SharePoint Designer (so called SPD Workflows). It’s interesting that the exception was rising for both workflow types. After some investigation using .Net Reflector, we discovered that the AssociationData xml-element is a cause of our troubles. The AssociationData is an element of the Workflow Definition Schema and specifies any custom data to pass to the workflow association form. Additionally, we use the association data when starting workflow on list items through the code. The workflow infrastructure, in SharePoint 2010, supposes that the AssociationData element is a valid xml string. So, if it’s empty or contains any data without xml-tags, the exception is thrown as opposite to SP 2007.

The solution consists of two steps. Firstly, for every Workflow Definition in your project you need to set <AssociationData> so that it contains a valid xml. Regardless of whether association data is actually used or not, <AssociationData> has to be presented in Workflow Definition and contain at least a fake valid xml. For example, I set dummy <Data />. In case you deploy a custom workflow through a SharePoint Feature, the Workflow Definition is usually located in Elements.xml file. If <AssociationData> element doesn’t exist within <Workflow> tag, add it at the same level as <MetaData>. The resultant workflow definition should look like:

<?xml version="1.0" encoding="utf-8" ?> 
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Workflow ...>
    ...
    <AssociationData>
      <Data />
    </AssociationData>
    <MetaData>
      ...
    </MetaData>
    ...
  </Workflow>
</Elements>

The described first step of the solution affects only new applications created after the alterations have been applied to Workflow Definitions. Even if a SharePoint Feature containing a custom workflow is reactivated, changes are only applied to workflow instances that start after the workflow association is modified. But for live applications created before or migrated from previous version of SharePoint (like in my case), the first step is useless. Additionally, you may have the SPD workflow which doesn’t include usual Workflow Definition at all. So, the second step is a necessary and sufficient part of the solution.

The second step is get all SPWorkflowAssociation objects and adjust their AssociationData properties programmatically. I’ve developed a few methods allowing to achieve this goal. See the listing below:

using System;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;

...

/// <summary>
/// Adjusts AssociationData properties of all Workflow Associations related to the passed Web, its child Lists and Content Types
/// </summary>
public static void AdjustAllWorkflowAssociations(SPWeb web)
{
    AdjustWebWorkflowAssociation(web);

    for (int i = 0; i < web.ContentTypes.Count; i ++)
        AdjustContentTypeWorkflowAssociation(web.ContentTypes[i]);

    for (int i = 0; i < web.Lists.Count; i ++)
    {
        AdjustListWorkflowAssociation(web.Lists[i]);
        for (int j = 0; j < web.Lists[i].ContentTypes.Count; j ++)
            AdjustContentTypeWorkflowAssociation(web.Lists[i].ContentTypes[j]);
    }
}

/// <summary>
/// Adjusts AssociationData properties of all Workflow Associations related to the passed List
/// </summary>
public static void AdjustListWorkflowAssociation(SPList list)
{
    AdjustAssociationData(list.WorkflowAssociations);
}

/// <summary>
/// Adjusts AssociationData properties of all Workflow Associations related to the passed Web
/// </summary>
public static void AdjustWebWorkflowAssociation(SPWeb web)
{
    AdjustAssociationData(web.WorkflowAssociations);
}

/// <summary>
/// Adjusts AssociationData properties of all Workflow Associations related to the passed Content Type
/// </summary>
public static void AdjustContentTypeWorkflowAssociation(SPContentType contentType)
{
    AdjustAssociationData(contentType.WorkflowAssociations);
}

/// <summary>
/// Adjusts AssociationData properties of all Workflow Associations in the passed collection
/// </summary>
public static void AdjustAssociationData(SPWorkflowAssociationCollection collection)
{
    for (int i = 0; i < collection.Count; i ++)
        AdjustAssociationData(collection[i], collection);
}

/// <summary>
/// Sets AssociationData property if it's not valid
/// </summary>
public static void AdjustAssociationData(SPWorkflowAssociation workflowAssociation, SPWorkflowAssociationCollection collection)
{
    if (!IsValidXml(workflowAssociation.AssociationData))
    {
        string newValue = string.IsNullOrEmpty(workflowAssociation.AssociationData)
                                ? "<Data />"
                                : string.Format("<Data>{0}</Data>", workflowAssociation.AssociationData);
        workflowAssociation.AssociationData = newValue;
        collection.Update(workflowAssociation);
    }
}

/// <summary>
/// Checks if the passed string is a valid xml
/// </summary>
public static bool IsValidXml(string str)
{
    if (!string.IsNullOrEmpty(str))
    {
        try
        {
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(str);
            return true;
        }
        catch {}
    }
    return false;
}

The basic method in the set is AdjustAssociationData, which examines AssociationData property of a passed SPWorkflowAssociation object and then applies a valid xml-value to the property if it’s necessary. As we know, workflows can be associated with webs, lists or content types. The AdjustWebWorkflowAssociation, AdjustListWorkflowAssociation and AdjustContentTypeWorkflowAssociation methods adjust AssociationData of a passed SPWeb, SPList or SPContentType object respectively. Finally, the AdjustAllWorkflowAssociations tries to adjust all available workflow associations that can be reached through a passed SPWeb instance.

Note that I everywhere used the for-statement instead of foreach, because otherwise the exception “Collection was modified; enumeration operation may not execute” will be thrown.

Below is the piece of code from the simple console application, which I used against our problem SharePoint application:

static void Main(string[] args)
{
    try
    {
        AppArguments arguments = new AppArguments(args);

        if(string.IsNullOrEmpty(arguments.Url))
        {
            Console.WriteLine("Invalid application URL!");
            return;
        }

        using (SPSite spSite = new SPSite(arguments.Url))
            using (SPWeb spWeb = spSite.OpenWeb())
                AdjustAllWorkflowAssociations(spWeb);

    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

Where the AppArguments is a class derived from InputArguments. InputArguments is described here – Simple Command Line Arguments Parser.

public class AppArguments : InputArguments
{
    public string Url
    {
        get { return GetValue("url"); }
    }

    public AppArguments(string[] args) : base(args)
    {
    }
}

As for our SharePoint application, it has only workflows associated with lists. So, after executing of the console app described above, the exception in question is gone. I hope it’ll help somebody as well.

The full version of the console app you can download here (Visual Studio 2010 solution). To execute it use the command line with parameters as follows:

AdjustAssociationData.exe -url "http://yourservername/yourapppath"

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: Remove duplicated fields from a Content Type

March 25th, 2012 No comments

    After in-place upgrading one of our SharePoint applications, we had been faced with the fact that some content types comprised duplicated fields. In other words, within the several <ContentType> sections of a list’s schema, we could find the pair <FieldRef> nodes with identical identifiers and names. Schematically, it looked like the following:

<?xml version="1.0" encoding="utf-8"?>
<List Name="SomeList" Title="Some List" BaseType="0" Url="Lists/SomeList" 
           Type="100" Id="a5bba3b3-5b1d-4186-ada7-bbd82b17f76d" ...>
  <MetaData>
    <Views>
       ...
    </Views>

    <Fields>
       ...
    </Fields>

    <ContentTypes>
       ...
       <ContentType ID="0x0100078C8A39971A4532AB9C5EB6DCB388A3" Name="SomeContentType" ...>
        <FieldRefs>
           ...
           <FieldRef Name="SomeField" ID="{b986cd1a-8bd0-4072-93af-5c48571bbf56}" />
           ...
           <FieldRef Name="SomeField2" ID="{6d245d53-63ef-4650-b676-6e4ee66dcda5}" />
           ...           
           <FieldRef Name="SomeField2" ID="{6d245d53-63ef-4650-b676-6e4ee66dcda5}" />
           ...   
           <FieldRef Name="SomeField" ID="{b986cd1a-8bd0-4072-93af-5c48571bbf56}" />
           ...
        </FieldRefs>
       ...    
    </ContentTypes>

    <Forms>
       ...
    </Forms>
   </MetaData>
</List>

The reason of such duplication still isn’t clear for me, but I’ve figured out how to get rid of it 🙂 Below is a simple method for deleting the excess fields from a passed content type:

protected static void RemoveDuplicatedFields(SPContentType spContentType)
{
    bool duplicationFound = false;

    // identify how many times every field encounter
    Dictionary<Guid, int> tmpDir = new Dictionary<Guid, int>();
    foreach (SPFieldLink spFieldLink in spContentType.FieldLinks)
        if (!tmpDir.ContainsKey(spFieldLink.Id))
            tmpDir.Add(spFieldLink.Id, 1);
        else
        {
            tmpDir[spFieldLink.Id]++;
            duplicationFound = true;
        }
   

    if (duplicationFound)
        // remove all excess mentions of fields
        foreach (KeyValuePair<Guid, int> keyValuePair in tmpDir)
        {
            int removeIterationCount = keyValuePair.Value - 1;
            for (int i = 0; i < removeIterationCount; i++)
                spContentType.FieldLinks.Delete(keyValuePair.Key);
        }
}

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.