Click here to Skip to main content
15,890,527 members
Articles / Programming Languages / C#

A Keyboard Selection Enabled ComboBox in Silverlight

Rate me:
Please Sign up or sign in to vote.
4.27/5 (9 votes)
24 Mar 2010CPOL3 min read 54.8K   519   10   13
Currently, ComboBoxItems cannot be selected using the keyboard - only the mouse. This is an attempt to resolve that.

Introduction

Currently, in Silverlight 3, ComboBox and ComboBoxItems cannot be selected via the keyboard much like it can be in any major language. In Windows Forms, HTML, Flash/Flex applications, etc., a user can select a ComboBox/ListBox/Select box and by typing in keys, can select the desired input. This poses a problem for Accessibility reasons, and may turn away some developers as a result.

Many existing solutions available on the internet make use of 3rd party DLLs, which may not be an option for some developers. This solution consists of writing a straight-forward extension method.

Background

It has become a common expectation that a user may access all ComboBox's items via the keyboard - an important accessibility feature. Tabbing to a control, they should be able to begin typing and retrieve the desired ComboBoxItem. This functionality does not exist in Silverlight by default, and thus needs to be written.

Some implementations get the user so far as selecting the first matching item, but not beyond that, and only when the ComboBox is closed. Thus in a list containing "Alabama," "Alaska" and "Arkansas", typing the letter "a" would only get the user as far as "Alabama." Typing in "L" would not yield "Alabama" or "Alaska" but "Louisiana." However, because it is important to maintain the user's expectations, and thus the user should be able to select "Alaska" by typing in: a-l-a-s.

Using the Code

Any intermediate developer familiar with C# and Silverlight should have an easy time implementing the following code.

Before listing the code, the implementation is as follows:

C#
string[] state = new string[] 
    { "Alabama", "Alaska", "Arizona", "Arkansas", "Delaware", "Louisiana", "Maine" };

ComboBox comboBox = new ComboBox();

for (int i = 0; i < state.Length; i++)
{
    ComboBoxItem comboBoxItem = new ComboBoxItem();
    comboBoxItem.Content = state[i];
    comboBox.Items.Add(comboBoxItem);
}

//Must enable keyboard selection **AFTER** it all items have bene added to the ComboBox
comboBox.SetKeyboardSelection(true);

//To disable keyboard selection...
//this is useful if the items of a selection box change - 
//keyboard selection should be reset.
comboBox.SetKeyboardSelection(false);

As is noted in the above comment, the SetKeyboardSelection() must only be called after all ComboBoxItems have been added to the ComboBox control.

The are two methods that allow for keyboard selection. SetKeyboardSelection() is the only one that needs to be called to enable selection via the keyboard. The other method, KeyPressSearch() is declared inside SetKeyboardSelection and is used to do the actual searching:

C#
public static class Extensions
{
    /*
     * SetKeyboardSelection enables keyboard selection on all
     * ComboBoxItems, as well as on the ComboBox itself (it has not already been added).
     * In addition, it tracks the "search history" that is created as the user types.
     * This is done to allow the user to type in more letters to narrow down
     * results (ie. "Ala" = Alabama, Alaska; "Alab" = Alabama)
     */
    public static void SetKeyboardSelection(this ComboBox comboBox, bool enable)
    {
        string searchStringEnabled = "KeyboardSelectionEnabled";
        string comboBoxTag = comboBox.Tag==null? "" : comboBox.Tag.ToString();
        //See if our search history control already exists 
        //by analyzing the combobox tag...
        bool isKeyboardEnabled = comboBoxTag.Contains(searchStringEnabled);

        /*
         * KeyPressSearch is defined as an anonymous delegate, 
         * that SetKeyboardSelection delegates
         * to the KeyUp events of ComboBoxItems and the parent ComboBox.
         */
        #region KeyPressSearch
        KeyEventHandler keyPressSearch = delegate(object sender, KeyEventArgs e)
        {
            //Since Key has only certain values, A-Z, D0-D9, NumPad0-9, Space, etc. 
            //let's just focus on letters, and numbers, and ignore all other keys... 
            //if they're pressed, clear the search history another option is to 
            //use PlatformKeyCode, but since it's platform specific, let's not.
            string key = e.Key.ToString();
            if (key.Length > 1 && (key.StartsWith("D") || key.StartsWith("NumPad")))
            { //remove the D/NumPad prefix to get the digit
                key = key.Replace("NumPad", "").Replace("D", "");
            }
            else if (key.Length > 1)
            {
                comboBox.Tag = searchStringEnabled + "||";
                return;
            }
            string searchHistoryPartsString = comboBox.Tag == 
		null ? searchStringEnabled + "||" : comboBox.Tag.ToString();
            string[] searchHistoryParts = (searchHistoryPartsString.Contains("|")) ? 
		searchHistoryPartsString.Split('|') : new string[0];

            int historyExpiration = 1500; 	//In 1.5 seconds, clear the history, 
					//and start new...
            string searchStringHistory = searchHistoryParts.Length == 3 ? 
					searchHistoryParts[1] : "";
            string searchStringTimeStampString = searchHistoryParts.Length == 3 ? 
					searchHistoryParts[2] : "";
            DateTime searchStringTimeStamp;
            string searchString = key;

            if (DateTime.TryParse(searchStringTimeStampString, out searchStringTimeStamp)
                && DateTime.Now.Subtract
		(searchStringTimeStamp).TotalMilliseconds < historyExpiration)
            {   //search history is valid and has not yet expired...
                searchString = searchStringHistory + key;
            }

            for (int i = 0; i < comboBox.Items.Count; i++)
            {
                if (comboBox.Items[i].GetType() == typeof(ComboBoxItem) &&
                    ((ComboBoxItem)comboBox.Items[i]).Content.ToString().StartsWith
			(searchString, StringComparison.InvariantCultureIgnoreCase))
                {
                    comboBox.SelectedIndex = i;
                    comboBox.Tag = searchStringEnabled + "|" + 
				searchString + "|" + DateTime.Now;
                    break;
                }
            }
        };
        #endregion

        if (!isKeyboardEnabled && enable)
        {
            comboBox.Tag = searchStringEnabled + "||";

            //Reset the search history on open and close
            comboBox.DropDownOpened += delegate
            {
                comboBox.Tag = searchStringEnabled + "||";
            };
            comboBox.DropDownClosed += delegate
            {
                comboBox.Tag = searchStringEnabled + "||";
            };

            //Add handler to parent control, so that we search even 
            //when combobox is closed, yet focused
            comboBox.KeyUp += keyPressSearch;

            for (int i = 0; i < comboBox.Items.Count; i++)
            {
                if (comboBox.Items[i].GetType() == typeof(ComboBoxItem))
                {
                    ((ComboBoxItem)comboBox.Items[i]).KeyUp += keyPressSearch;
                }
            }
        }
        else if (isKeyboardEnabled && !enable)
        {
            //Remove handler
            comboBox.KeyUp -= keyPressSearch;
            for (int i = 0; i < comboBox.Items.Count; i++)
            {
                if (comboBox.Items[i].GetType() == typeof(ComboBoxItem))
                {
                    ((ComboBoxItem)comboBox.Items[i]).KeyUp -= keyPressSearch;
                }
            }
            comboBox.Tag = "";
        }
        else
        {
            //Remove handler
            comboBox.KeyUp -= keyPressSearch;
            comboBox.Tag = "";
        }
    }
}

The way all this works is that each ComboBoxItem is given a KeyUp event that conducts the search. This way, the searching can be done regardless of whether the ComboBox is open or closed. The current search history, which is preserved in the ComboBox Tag property is stored for 1.5 seconds (if the user searches "A-l-a" and then pauses two seconds and starts searching "D", they will be conducting a new search.)

Points of Interest

It's worth pointing out that the KeyPressSearch() method uses the KeyEventArgs object, which exposes two ways to obtain the key pressed, the enum KeyEventArgs.Key, which is defined in System.Windows.Input.Key and KeyEventArgs.PlatformKeyCode which is an integer that is platform-specific.

There is a trade-off here. Because System.Windows.Input.Key is rather limited in the keys it exposes, searching is limited to alpha-numeric, and thus special characters (!, -, +, @, #, $ etc.) are ignored. Some of these characters can easily be accepted, but not all.

Using KeyEventArgs.PlatformKeyCode allows a greater range of keys, however, because the key-codes are platform specific, greater consideration needs to be made when accepting and denying ranges, as it depends on what OS a user is using.

History

  • Changed implementation to store history in Tag property for better results
  • Added ability to remove keyboard selection, and re-add it. This is useful for when items refresh, or change. In this case, keyboard selection should be removed and added once more
  • Resolved bug which occurred when using multiple ComboBoxes
  • Last change: March 24, 2010

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionWhy do you use exactly 1500 ms? Pin
Member 338957012-May-12 22:37
Member 338957012-May-12 22:37 
QuestionCombobox is binded to a IDictionary<int,string> Pin
Santoshf212-Mar-12 1:38
Santoshf212-Mar-12 1:38 
GeneralMy vote of 2 Pin
joe brockhaus23-Dec-10 8:27
joe brockhaus23-Dec-10 8:27 
GeneralI've improved your code for the case when ComboBox bound to IEnumerable of Entity Pin
sip_slava3-Nov-10 16:44
sip_slava3-Nov-10 16:44 
QuestionWhat to do when using an ItemTemplate? Pin
milund19-Sep-10 12:17
milund19-Sep-10 12:17 
AnswerRe: What to do when using an ItemTemplate? [modified] Pin
Alishah Novin20-Sep-10 6:12
Alishah Novin20-Sep-10 6:12 
Hi,

The only way that would really work is if the items you're adding have Key events. I've started writing a custom ComboBox that inherits from the base ComboBox. It's used exactly the same way you would with a ComboBox - you bind your items to the ItemsSource, and set the DisplayMemberPath. It then uses reflection to find the object you're looking for when you start typing. It uses a PropertyReflector class I found online which works very well.

Here's the code - keep in mind though, it's only been my first pass at it. I know it has a few issues (for one, the binding of the DisplayMemberPath isn't working...)

Feel free to clean up the code as you like, add your changes, and release it!

XAML
<ComboBox x:Class="SilverlightApp.Client.Controls.KeyboardEnabledComboBox"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" />


Code Behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace SilverlightApp.Client.Controls
{
    private Dictionary<ComboBoxItem, object> _ComboBoxItemValues = new Dictionary<ComboBoxItem, object>();

        private int _HistoryResetDelay = 1500;
        public int HistoryResetDelay
        {
            get { return _HistoryResetDelay / 1000; }
            set { _HistoryResetDelay = value * 1000; }
        }

        private System.Collections.IEnumerable _ItemsSource;
        public new System.Collections.IEnumerable ItemsSource
        {
            get
            {
                return _ItemsSource;
            }
            set
            {
                _ItemsSource = value;
                _ComboBoxItemValues.Clear();
                this.Items.Clear();
                foreach (object item in _ItemsSource)
                {
                    ComboBoxItem comboBoxItem = new ComboBoxItem();

                    if (string.IsNullOrEmpty(this.DisplayMemberPath))
                    {
                        comboBoxItem.Content = item;
                    }
                    else
                    {
                        System.Windows.Data.Binding binding = new System.Windows.Data.Binding(this.DisplayMemberPath);
                        binding.Source = item;
                        comboBoxItem.SetBinding(ComboBoxItem.ContentProperty, binding);
                    }

                    _ComboBoxItemValues.Add(comboBoxItem, item);
                    this.Items.Add(comboBoxItem);
                }
                IsKeboardSelectionEnabled = _IsKeboardSelectionEnabled;
            }
        }
        
        public new object SelectedItem
        {
            get
            {
                ComboBoxItem comboBoxItem = (ComboBoxItem)this.Items[this.SelectedIndex];
                if (_ComboBoxItemValues.ContainsKey(comboBoxItem)) {
                    return this._ComboBoxItemValues[comboBoxItem];
                }
                return null;
            }
            set
            {
                foreach (ComboBoxItem comboBoxItem in this._ComboBoxItemValues.Keys)
                {
                    if (this._ComboBoxItemValues[comboBoxItem] == value)
                    {
                        comboBoxItem.IsSelected = true;
                        break;
                    }
                }
            }
        }

        public KeyboardEnabledComboBox() : base()
        {
            IsKeboardSelectionEnabled = _IsKeboardSelectionEnabled;

            this.Loaded += new RoutedEventHandler(KeyboardEnabledComboBox_Loaded);
            this.LayoutUpdated += new EventHandler(KeyboardEnabledComboBox_LayoutUpdated);
        }

        private void KeyboardEnabledComboBox_LayoutUpdated(object sender, EventArgs e)
        {
            IsKeboardSelectionEnabled = _IsKeboardSelectionEnabled;
            this.LayoutUpdated -= KeyboardEnabledComboBox_LayoutUpdated;
        }

        private void KeyboardEnabledComboBox_Loaded(object sender, RoutedEventArgs e)
        {
            IsKeboardSelectionEnabled = _IsKeboardSelectionEnabled;
            this.Loaded -= KeyboardEnabledComboBox_Loaded;
        }

        private bool _IsKeboardSelectionEnabled = true;
        public bool IsKeboardSelectionEnabled
        {
            get
            {
                return _IsKeboardSelectionEnabled;
            }
            set
            {
                if (_IsKeboardSelectionEnabled != value)
                {
                    SetKeyboardSelection(value);
                }
                else
                {
                    //Reset by flipping switch
                    SetKeyboardSelection(!value);
                    SetKeyboardSelection(value);
                }
            }
        }


        private string _SearchHistory = string.Empty;
        private DateTime _SearchHistoryTimestamp = DateTime.MinValue;

        private void SetKeyboardSelection(bool enable)
        {
            _IsKeboardSelectionEnabled = enable;
            if (enable)
            {
                //Reset the search history on open and close
                this.DropDownOpened += new EventHandler(DropDownChange);
                this.DropDownClosed += new EventHandler(DropDownChange);

                //Add handler to parent control, so that we search even when combobox is closed, yet focused
                this.KeyUp += KeyPressSearch;

                for (int i = 0; i < this.Items.Count; i++)
                {
                    if (this.Items[i].GetType() == typeof(ComboBoxItem))
                    {
                        ComboBoxItem comboBoxItem = (ComboBoxItem)this.Items[i];
                        comboBoxItem.KeyUp += KeyPressSearch;

                        if (!_ComboBoxItemValues.ContainsKey(comboBoxItem))
                        {
                            _ComboBoxItemValues.Add(comboBoxItem, comboBoxItem);
                        }
                    }
                }
            }
            else
            {
                //Remove handler
                this.KeyUp -= KeyPressSearch;
                for (int i = 0; i < this.Items.Count; i++)
                {
                    if (this.Items[i].GetType() == typeof(ComboBoxItem))
                    {
                        ComboBoxItem comboBoxItem = (ComboBoxItem)this.Items[i];
                        comboBoxItem.KeyUp -= KeyPressSearch;

                        if (!_ComboBoxItemValues.ContainsKey(comboBoxItem))
                        {
                            _ComboBoxItemValues.Add(comboBoxItem, comboBoxItem);
                        }
                    }
                }
            }

            ResetSearch();
        }

        private void DropDownChange(object sender, EventArgs e)
        {
            ResetSearch();
        }

        private void KeyPressSearch(object sender, KeyEventArgs e)
        {
            //Since Key has only certain values, A-Z, D0-D9, NumPad0-9, Space, etc. let's just focus on
            //letters, and numbers, and ignore all other keys... if they're pressed, clear the search history
            //another option is to use PlatformKeyCode, but since it's platform specific, let's not.
            string key = e.Key.ToString();
            if (key.Length > 1 && (key.StartsWith("D") || key.StartsWith("NumPad")))
            { //remove the D/NumPad prefix to get the digit
                key = key.Replace("NumPad", "").Replace("D", "");
            }
            else if (key.Length > 1)
            {
                ResetSearch();
                return;
            }

            string searchString = (DateTime.Now.Subtract(_SearchHistoryTimestamp).TotalMilliseconds < _HistoryResetDelay) ? _SearchHistory + key : key;

            for (int i = 0; i < this.Items.Count; i++)
            {
                string comboBoxItemText = string.IsNullOrEmpty(this.DisplayMemberPath) ? ((ComboBoxItem)this.Items[i]).Content.ToString() : PropertyReflector.Instance.GetValue(_ComboBoxItemValues[(ComboBoxItem)this.Items[i]], this.DisplayMemberPath).ToString();
                if (comboBoxItemText.StartsWith(searchString, StringComparison.InvariantCultureIgnoreCase))
                {
                    this.SelectedIndex = i;
                    _SearchHistory = searchString;
                    _SearchHistoryTimestamp = DateTime.Now;
                    return;
                }
            }

            ResetSearch();   
        }

        private void ResetSearch()
        {
            _SearchHistory = string.Empty;
            _SearchHistoryTimestamp = DateTime.MinValue;
        }
    }

    /// <summary> 
    /// Helps to get and set property values on objects through reflection. 
    /// Properties of underlying objects can be accessed directly by separating 
    /// the levels in the hierarchy by dots. 
    /// To get/set the name of an Ancestor, for objects that have a Parent property, 
    /// you could use "Parent.Parent.Parent.Name". 
    /// </summary> 
    internal class PropertyReflector
    {
        private static readonly PropertyReflector _Instance = new PropertyReflector();

        public static PropertyReflector Instance { get { return _Instance; } }

        private const char PropertyNameSeparator = '.';

        private static readonly object[] NoParams = new object[0];
        private static readonly Type[] NoTypeParams = new Type[0];

        private IDictionary<Type, PropertyInfoCache> propertyCache = new Dictionary<Type, PropertyInfoCache>();
        private IDictionary<Type, System.Reflection.ConstructorInfo> constructorCache = new Dictionary<Type, System.Reflection.ConstructorInfo>();

        private PropertyReflector()
        {
        }

        public static bool IsGenericList(Type type)
        {
            foreach (Type testinterface in type.GetInterfaces())
            {
                if (testinterface.IsGenericType)
                {
                    if (testinterface.GetGenericTypeDefinition() == typeof(System.Collections.ICollection))
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        /// <summary> 
        /// Gets the Type of the given property of the given targetType. 
        /// The targetType and propertyName parameters can't be null. 
        /// </summary> 
        /// <param name="targetType">the target type which contains the property</param> 
        /// <param name="propertyName">the property to get, can be a property on a nested object (eg. "Child.Name")</param>

        public Type GetType(Type targetType, string propertyName)
        {
            if (propertyName.IndexOf(PropertyNameSeparator) > -1)
            {
                string[] propertyList = propertyName.Split(PropertyNameSeparator);
                for (int i = 0; i < propertyList.Length; i++)
                {
                    string currentProperty = propertyList[i];
                    targetType = GetTypeImpl(targetType, currentProperty);
                }
                return targetType;
            }
            else
            {
                return GetTypeImpl(targetType, propertyName);
            }

        }

        /// <summary> 
        /// Gets the value of the given property of the given target. 
        /// If objects within the property hierarchy are null references, null will be returned. 
        /// The target and propertyName parameters can't be null. 
        /// </summary> 
        /// <param name="target">the target object to get the value from</param> 
        /// <param name="propertyName">the property to get, can be a property on a nested object (eg. "Child.Name")</param>


        public object GetValue(object target, string propertyName)
        {
            if (propertyName.IndexOf(PropertyNameSeparator) > -1)
            {
                string[] propertyList = propertyName.Split(PropertyNameSeparator);
                for (int i = 0; i < propertyList.Length; i++)
                {
                    string currentProperty = propertyList[i];

                    //Check for Index here
                    int startindex = currentProperty.IndexOf('[');
                    if (startindex > -1)
                    {
                        int index = 0;
                        //Get Index
                        string indexstring = currentProperty.Substring(startindex + 1, currentProperty.IndexOf(']') - (startindex + 1));
                        string collectionname = currentProperty.Substring(0, startindex);

                        System.Reflection.PropertyInfo propertyInfo = GetPropertyInfo(target.GetType(), collectionname);
                        if (propertyInfo != null && typeof(System.Collections.IEnumerable).IsAssignableFrom(propertyInfo.PropertyType))
                        {
                            var test = GetValueImpl(target, collectionname);

                            if (Int32.TryParse(indexstring, out index))
                            {
                                target = GetElementAt(test, index);
                            }
                        }
                    }
                    else
                    {
                        target = GetValueImpl(target, currentProperty);
                    }


                    if (target == null)
                    {
                        return null;
                    }
                }
                return target;
            }
            else
            {
                return GetValueImpl(target, propertyName);
            }
        }

        private object GetElementAt(object target, int index)
        {
            Type tObject = target.GetType();
            Type t = tObject.GetGenericArguments()[0];

            Type tIEnumerable = typeof(IEnumerable<>).MakeGenericType(t);

            System.Reflection.MethodInfo mElementAt =
                typeof(Enumerable)
                .GetMethod("ElementAt")
                .MakeGenericMethod(t);

            return mElementAt.Invoke(null, new Object[] { target, index });
        }

        /// <summary> 
        /// Sets the value of the given property on the given target to the given value. 
        /// If objects within the property hierarchy are null references, an attempt will be 
        /// made to construct a new instance through a parameterless constructor. 
        /// The target and propertyName parameters can't be null. 
        /// </summary> 
        /// <param name="target">the target object to set the value on</param> 
        /// <param name="propertyName">the property to set, can be a property on a nested object (eg. "Child.Name")</param>

        /// <param name="value">the new value of the property</param> 
        public void SetValue(object target, string propertyName, object value)
        {
            if (propertyName.IndexOf(PropertyNameSeparator) > -1)
            {
                object originalTarget = target;
                string[] propertyList = propertyName.Split(PropertyNameSeparator);
                for (int i = 0; i < propertyList.Length - 1; i++)
                {
                    propertyName = propertyList[i];
                    target = GetValueImpl(target, propertyName);
                    if (target == null)
                    {
                        string currentFullPropertyNameString = GetPropertyNameString(propertyList, i);
                        target = Construct(GetType(originalTarget.GetType(), currentFullPropertyNameString));
                        SetValue(originalTarget, currentFullPropertyNameString, target);
                    }
                }
                propertyName = propertyList[propertyList.Length - 1];
            }
            SetValueImpl(target, propertyName, value);
        }

        /// <summary> 
        /// Returns a string containing the properties in the propertyList up to the given 
        /// level, separated by dots. 
        /// For the propertyList { "Zero", "One", "Two" } and level 1, the string 
        /// "Zero.One" will be returned. 
        /// </summary> 
        /// <param name="propertyList">the array containing the properties in the corect order</param> 
        /// <param name="level">the level up to wich to include the properties in the returned string</param> 
        /// <returns>a dot-separated string containing the properties up to the given level</returns> 
        private static string GetPropertyNameString(string[] propertyList, int level)
        {
            System.Text.StringBuilder currentFullPropertyName = new System.Text.StringBuilder();
            for (int j = 0; j <= level; j++)
            {
                if (j > 0)
                {
                    currentFullPropertyName.Append(PropertyNameSeparator);
                }
                currentFullPropertyName.Append(propertyList[j]);
            }
            return currentFullPropertyName.ToString();
        }

        /// <summary> 
        /// Returns the type of the given property on the target instance. 
        /// The type and propertyName parameters can't be null. 
        /// </summary> 
        /// <param name="targetType">the type of the target instance</param> 
        /// <param name="propertyName">the property to retrieve the type for</param> 
        /// <returns>the typr of the given property on the target type</returns> 
        private Type GetTypeImpl(Type targetType, string propertyName)
        {
            return GetPropertyInfo(targetType, propertyName).PropertyType;
        }

        /// <summary> 
        /// Returns the value of the given property on the target instance. 
        /// The target instance and propertyName parameters can't be null. 
        /// </summary> 
        /// <param name="target">the instance on which to get the value</param> 
        /// <param name="propertyName">the property for which to get the value</param> 
        /// <returns>the value of the given property on the target instance</returns> 
        private object GetValueImpl(object target, string propertyName)
        {
            return GetPropertyInfo(target.GetType(), propertyName).GetValue(target, NoParams);
        }

        /// <summary> 
        /// Sets the given property of the target instance to the given value. 
        /// Type mismatches in the parameters of these methods will result in an exception. 
        /// Also, the target instance and propertyName parameters can't be null. 
        /// </summary> 
        /// <param name="target">the instance to set the value on</param> 
        /// <param name="propertyName">the property to set the value on</param> 
        /// <param name="value">the value to set on the target</param> 
        private void SetValueImpl(object target, string propertyName, object value)
        {
            GetPropertyInfo(target.GetType(), propertyName).SetValue(target, value, NoParams);
        }

        /// <summary> 
        /// Obtains the PropertyInfo for the given propertyName of the given type from the cache. 
        /// If it is not already in the cache, the PropertyInfo will be looked up and added to 
        /// the cache. 
        /// </summary> 
        /// <param name="type">the type to resolve the property on</param> 
        /// <param name="propertyName">the name of the property to return the PropertyInfo for</param> 
        /// <returns></returns> 
        private System.Reflection.PropertyInfo GetPropertyInfo(Type type, string propertyName)
        {
            PropertyInfoCache propertyInfoCache = GetPropertyInfoCache(type);
            if (!propertyInfoCache.ContainsKey(propertyName))
            {
                System.Reflection.PropertyInfo propertyInfo = GetBestMatchingProperty(propertyName, type);
                if (propertyInfo == null)
                {
                    throw new ArgumentException(string.Format("Unable to find public property named {0} on type {1}", propertyName, type.FullName), propertyName);

                }
                propertyInfoCache.Add(propertyName, propertyInfo);
            }
            return propertyInfoCache[propertyName];
        }

        /// <summary> 
        /// Gets the best matching property info for the given name on the given type if the same property is defined on 
        /// multiple levels in the object hierarchy. 
        /// </summary> 
        private static System.Reflection.PropertyInfo GetBestMatchingProperty(string propertyName, Type type)
        {
            System.Reflection.PropertyInfo[] propertyInfos = type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.FlattenHierarchy);

            System.Reflection.PropertyInfo bestMatch = null;
            int bestMatchDistance = int.MaxValue;
            for (int i = 0; i < propertyInfos.Length; i++)
            {
                System.Reflection.PropertyInfo info = propertyInfos[i];
                if (info.Name == propertyName)
                {
                    int distance = CalculateDistance(type, info.DeclaringType);
                    if (distance == 0)
                    {
                        // as close as we're gonna get... 
                        return info;
                    }
                    if (distance > 0 && distance < bestMatchDistance)
                    {
                        bestMatch = info;
                        bestMatchDistance = distance;
                    }
                }
            }
            return bestMatch;
        }

        /// <summary> 
        /// Calculates the hierarchy levels between two classes. 
        /// If the targetObjectType is the same as the baseType, the returned distance will be 0. 
        /// If the two types do not belong to the same hierarchy, -1 will be returned. 
        /// </summary> 
        private static int CalculateDistance(Type targetObjectType, Type baseType)
        {
            if (!baseType.IsInterface)
            {
                Type currType = targetObjectType;
                int level = 0;
                while (currType != null)
                {
                    if (baseType == currType)
                    {
                        return level;
                    }
                    currType = currType.BaseType;
                    level++;
                }
            }
            return -1;
        }

        /// <summary> 
        /// Returns the PropertyInfoCache for the given type. 
        /// If there isn't one available already, a new one will be created. 
        /// </summary> 
        /// <param name="type">the type to retrieve the PropertyInfoCache for</param> 
        /// <returns>the PropertyInfoCache for the given type</returns> 
        private PropertyInfoCache GetPropertyInfoCache(Type type)
        {
            if (!propertyCache.ContainsKey(type))
            {
                lock (this)
                {
                    if (!propertyCache.ContainsKey(type))
                    {
                        propertyCache.Add(type, new PropertyInfoCache());
                    }
                }
            }
            return propertyCache[type];
        }

        /// <summary> 
        /// Creates a new object of the given type, provided that the type has a default (parameterless) 
        /// constructor. If it does not have such a constructor, an exception will be thrown. 
        /// </summary> 
        /// <param name="type">the type of the object to construct</param> 
        /// <returns>a new instance of the given type</returns> 
        private object Construct(Type type)
        {
            if (!constructorCache.ContainsKey(type))
            {
                lock (this)
                {
                    if (!constructorCache.ContainsKey(type))
                    {
                        System.Reflection.ConstructorInfo constructorInfo = type.GetConstructor(NoTypeParams);
                        if (constructorInfo == null)
                        {
                            throw new Exception(string.Format("Unable to construct instance, no parameterless constructor found in type {0}", type.FullName));

                        }
                        constructorCache.Add(type, constructorInfo);
                    }
                }
            }
            return constructorCache[type].Invoke(NoParams);
        }
    }

    /// <summary> 
    /// Keeps a mapping between a string and a PropertyInfo instance. 
    /// Simply wraps an IDictionary and exposes the relevant operations. 
    /// Putting all this in a separate class makes the calling code more 
    /// readable. 
    /// </summary> 
    internal class PropertyInfoCache
    {
        private IDictionary<string, System.Reflection.PropertyInfo> propertyInfoCache;

        public PropertyInfoCache()
        {
            propertyInfoCache = new Dictionary<string, System.Reflection.PropertyInfo>();
        }

        public bool ContainsKey(string key)
        {
            return propertyInfoCache.ContainsKey(key);
        }

        public void Add(string key, System.Reflection.PropertyInfo value)
        {
            propertyInfoCache.Add(key, value);
        }

        public System.Reflection.PropertyInfo this[string key]
        {
            get { return propertyInfoCache[key]; }
            set { propertyInfoCache[key] = value; }
        }
    }
}


modified on Monday, September 20, 2010 12:54 PM

GeneralRe: What to do when using an ItemTemplate? Pin
milund20-Sep-10 10:26
milund20-Sep-10 10:26 
GeneralVery Clever! Pin
rdamske13-May-10 10:04
professionalrdamske13-May-10 10:04 
GeneralRe: Very Clever! Pin
Alishah Novin13-May-10 10:46
Alishah Novin13-May-10 10:46 
Generalcombobox Pin
Lana_12-May-10 3:19
Lana_12-May-10 3:19 
GeneralRe: combobox Pin
Alishah Novin12-May-10 4:38
Alishah Novin12-May-10 4:38 
GeneralRe: combobox Pin
Lana_12-May-10 20:33
Lana_12-May-10 20:33 
GeneralNice work Pin
Mohd Arshad Malik19-Mar-10 10:23
Mohd Arshad Malik19-Mar-10 10:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.