Sharepoint "ListItemPropertyField" WebControl To Render Any Subproperty Of ListItem Field

Posted by Ahmed Tarek Hasan on 12/21/2012 12:57:00 AM with No comments
I was working on a Sharepoint 2010 project and I had a task to modify a custom page layout to include the e-mail of the article owner. I found that this article owner field is a custom field in the pages library and its type is "Person or Group".

So, I started to trace this field to find how I can get the email. I found that I can use an OOTB Sharepoint webcontrol called "BaseFieldControl" which has a property called "FieldName". When this property is set to the name of the field in a Sharepoint listitem, it fires its ToString() to get a string value for this field to be rendered.

But, this couldn't help me cause the email property I need is a subproperty of the listitem field. So, knowing that the field name is "ContentOwner", then to get the user email I will need to get "ContentOwner.User.Email". Actually, I will need to get "ContentOwner[0].User.Email" cause ContentOwner is a collection.

So, I searched for something OOTB to provide such capability but I couldn't and that's why I decided to make my own WebControl. This control should provide some powerful capabilities to be an asset to keep and reuse whenever needed.

So, I decided to prepare this control to have the following features:
  1. Get subproperty of a listitem field
  2. This field may be a collection, so need the ability to provide an index, if not provided assume zero (ie.: ContentOwner[1])
  3. Get multilevel subproperty of the field which could be represented by a properties pipeline (ie.: User.Email of ContentOwner[0])
  4. Be able to provide indexes for subproperties if they are collections or assume zero if indexes not provided (ie.: SubPropertyA[1].SubPropertyB[0].SubPropertyC[3] of Property)
  5. Be able to provide a type converter for the final subproperty in case of required custom conversion or manipulation code (ie.: like converting bool from true/false to yes/no or good/bad ......)
  6. Be able to provide a pattern to format the final output

So, I gave it a thought and then went deep into code -which I really enjoyed by the way :)- and I came out with the code below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Reflection;
using System.Collections;
using System.Web.UI;
using System.ComponentModel;
using Microsoft.SharePoint.WebControls;

[assembly: TagPrefix("DevelopmentSimplyPut.Sharepoint.WebControls", "DevelopmentSimplyPutControls")]
namespace DevelopmentSimplyPut.Sharepoint.WebControls
{
    [ParseChildren(ChildrenAsProperties = true)]
    [PersistChildren(false)]
    [ToolboxData("<{0}:ListItemPropertyField runat=\"server\"></{0}:ListItemPropertyField>")]
    public class ListItemPropertyField : BaseFieldControl, INamingContainer
    {
        public string Property
        {
            get;
            set;
        }

        public string RenderTemplate
        {
            get;
            set;
        }

        public string ItemIndex
        {
            get;
            set;
        }

        [PersistenceMode(PersistenceMode.InnerProperty)]
        public TypeConverter PropertyTypeConverter
        {
            get;
            set;
        }

        public override void UpdateFieldValueInItem()
        {

        }

        protected override void Render(System.Web.UI.HtmlTextWriter output)
        {
            try
            {
                object fieldValue = this.ListItem[this.FieldName];
                fieldValue = GetItemFromCollectionByIndex(fieldValue, ItemIndex);

                string subPropertyValue = string.Empty;
                object subPropertyObject = null;
                if (!string.IsNullOrEmpty(Property))
                {
                    subPropertyObject = GetSubPropertyValue(fieldValue, Property);
                }
                else
                {
                    subPropertyObject = fieldValue;
                }

                if (null != PropertyTypeConverter)
                {
                    subPropertyObject = ApplyTypeConverter(subPropertyObject, PropertyTypeConverter);
                }

                if (!string.IsNullOrEmpty(RenderTemplate))
                {
                    subPropertyValue = string.Format(CultureInfo.InvariantCulture, RenderTemplate, subPropertyObject);
                }
                else
                {
                    subPropertyValue = subPropertyObject.ToString();
                }
                output.Write(subPropertyValue);
            }
            catch (NullReferenceException ex)
            {
                //Do something
            }
            catch (ArgumentOutOfRangeException ex)
            {
                //Do something
            }
            catch (ArgumentNullException ex)
            {
                //Do something
            }
            catch (ArgumentException ex)
            {
                //Do something
            }
        }

        private static object GetSubProperty(object item, string property, string index)
        {
            Type type = item.GetType();
            PropertyInfo propertyInfo = type.GetProperty(property);
            object value = propertyInfo.GetValue(item, null);

            return GetItemFromCollectionByIndex(value, index);
        }

        private static object GetSubPropertyValue(object item, string property)
        {
            object parentItem = item;
            if (!string.IsNullOrEmpty(property))
            {
                string[] parts = property.Split('.');
                for (int i = 0; i < parts.Length; i++)
                {
                    string[] subParts = parts[i].Split('[');
                    string propertyName = parts[i];
                    string index = "0";

                    if (subParts.Length > 1)
                    {
                        propertyName = subParts[0];
                        index = subParts[1].Replace("]", "");
                    }
                    else
                    {
                        propertyName = parts[i];
                    }

                    parentItem = GetSubProperty(parentItem, propertyName, index);
                }
            }

            return parentItem;
        }

        private static object GetItemFromCollectionByIndex(object searchCollection, string index)
        {
            ICollection collection = searchCollection as ICollection;
            object result = searchCollection;
            if (collection != null)
            {
                int requestedIndex = 0;
                int itemsCount = collection.Count;

                if (!string.IsNullOrEmpty(index))
                {
                    if (!int.TryParse(index, out requestedIndex))
                    {
                        requestedIndex = 0;
                    }
                }

                IEnumerator enumObject = collection.GetEnumerator();
                requestedIndex = Math.Max(0, requestedIndex);
                requestedIndex = Math.Min(itemsCount, requestedIndex);

                for (int x = 0; x < itemsCount; x++)
                {
                    enumObject.MoveNext();

                    if (x == requestedIndex)
                    {
                        result = enumObject.Current;
                        break;
                    }
                }
            }

            return result;
        }

        private static object ApplyTypeConverter(object property, TypeConverter converter)
        {
            object result = property;

            if (null != property && null != converter)
            {
                if (!string.IsNullOrEmpty(converter.AssemblyFullyQualifiedName) && !string.IsNullOrEmpty(converter.ClassFullyQualifiedName) && !string.IsNullOrEmpty(converter.MethodName))
                {
                    AssemblyName assemblyName = new AssemblyName(converter.AssemblyFullyQualifiedName);
                    Assembly assembly = Assembly.Load(assemblyName);
                    Type classType = assembly.GetType(converter.ClassFullyQualifiedName);
                    object[] parametersArray = new object[] { property };
                    result = classType.InvokeMember(converter.MethodName, System.Reflection.BindingFlags.InvokeMethod, System.Type.DefaultBinder, "", parametersArray);
                }
            }

            return result;
        }
    }

    public class TypeConverter
    {
        public string AssemblyFullyQualifiedName
        {
            get;
            set;
        }
        public string ClassFullyQualifiedName
        {
            get;
            set;
        }
        public string MethodName
        {
            get;
            set;
        }
    }
}

Notes
  1. The control is inherited from "BaseFieldControl" for extension
  2. The method "UpdateFieldValueInItem" is overridden by an empty method to prevent the control from updating the value of the field in the listitem. It took me some time to figure this out :) 
  3. I used reflection to get subproperties of the field property
  4. Some string manipulation is used to get indexes of subproperties
  5. The type converter is provided by setting three properties
    1. AssemblyFullyQualifiedName: this is the assembly fully qualified name (see the example below)
    2. ClassFullyQualifiedName: this is the fully qualified name of the class which includes the conversion method (see the example below)
    3. MethodName: this is the name of the conversion method (see the example below)
  6. To calculate the last value to be rendered, I first get the final subproperty value by reflection, then pass it to the type converter (if provided) and finally apply the formatting pattern (if provided) 
  7. I used the "Render" method to render the final value


So, now to use this control on a page layout, first we need to register the control on the page as follows:
<%@ Register Tagprefix="DevelopmentSimplyPutControls" Namespace="DevelopmentSimplyPut.Sharepoint.WebControls" Assembly="DevelopmentSimplyPut.Sharepoint.WebControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7a0271768eefdc39" %>

Then we can place the control on the page as follows:
<DevelopmentSimplyPutControls:ListItemPropertyField runat="server" id="ContentOwnerEmail" FieldName="ContentOwner" Property="User.Email" RenderTemplate="Email:{0}" ItemIndex="0">
 <PropertyTypeConverter MethodName="EmailCustomConverter" ClassFullyQualifiedName="DevelopmentSimplyPut.Utilities.TypeConverters" AssemblyFullyQualifiedName="DevelopmentSimplyPut.Utilities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7a0271768eefdc39"/>
</DevelopmentSimplyPutControls:ListItemPropertyField>

Notes
  1. The "Property" attribute is optional. If not provided, then the value returned will be the value of the field property, no subproperties
  2. The "ItemIndex" attribute is optional. If not provided, in case of collection field property, it will be assumed to be zero. I provided it here just for clarification but it could be ignored
  3. The "RenderTemplate" attribute is optional. If provided, it will be used to format the final value. Every "{0}" in the template will be replaced by the final value
  4. The "PropertyTypeConverter" inner property is optional. If provided, the control will load the conversion method from its assembly provided by reflection and apply this method on the final value
  5. For "PropertyTypeConverter" to work:
    1. The assembly provided should be in the GAC
    2. The class including the method should be static
    3. The conversion method should be static


Hope you find this control useful.
Good Luck



Categories: , ,