Click here to Skip to main content
15,894,646 members
Articles / Programming Languages / C#

Generic Multi-Field/Property Sorting for Lists of Business Objects

Rate me:
Please Sign up or sign in to vote.
4.89/5 (13 votes)
13 Feb 2008CPOL8 min read 75.7K   496   72  
This article presents a simple and flexible way to sort strongly-typed lists of business objects using multiple properties or fields.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.ComponentModel;

// Source code by Owen Emlen (owene_1998@yahoo.com, owen@binarynorthwest.com)

namespace BinaryNorthwest
{
    /// <summary>
    /// Provides quick access to property values and handles value comparisons. 
    /// Note: This class uses PropertyDescriptor.GetValue(object) to retrieve property information.  
    /// The speed of property retrieval has been greatly improved by the addition of Marc Gravell's code for
    /// HyperPropertyDescriptors, located at http://www.codeproject.com/csharp/HyperPropertyDescriptor.asp
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class CompareProperties<T> : IComparer<T> where T : class
    {
        private static Dictionary<int, string> FastEnumLookup;

        public SortType sortType = SortType.eUsePropertyOrFieldType;

        #region "Fields"
        /// <summary>
        /// Stores the cached property info and type, used to retrieve values
        /// Note: Retrieving property values used to involve invoking the property Get accessor
        /// and thus was significantly slower that retrieving a field value.  However, using Marc Gravell's
        /// HyperPropertyDescriptor code, property retrieval is now much faster than retrieval of field values.
        /// </summary>
        public PropertyInfo pi;
        public PropertyDescriptor property;

        internal Type typ;
        internal bool fFoundProperty;
        internal string sPropertyName;
        internal bool fSortDescending;
        internal StringComparison stringComparisonToUse = StringComparison.Ordinal;
        #endregion

        #region "Constructors"
        public CompareProperties(string sPropName, bool fDescendingSort)
        {
            sPropertyName = sPropName;
            fSortDescending = fDescendingSort;
        }

        public CompareProperties(string sPropName, bool fDescendingSort, SortType sortTyp)
        {
            sPropertyName = sPropName;
            fSortDescending = fDescendingSort;
            sortType = sortTyp;
        }
        #endregion

        #region "Manual Overrides"
        /// <summary>
        /// Override the sort type at your own peril.  If any field/property value can't be converted
        /// to the type you specify, an exception will be raised.
        /// </summary>
        /// <param name="sortTyp"></param>
        public void SetOverrideSortType(SortType sortTyp) { sortType = sortTyp; }

        /// <summary>
        /// Default string comparison type is Ordinal.  Using this method, you can specify
        /// other options, such as OrdinalIgnoreCase (for case insensitive comparison), etc
        /// </summary>
        /// <param name="stringComparisonType"></param>
        public void SetStringComparisonType(StringComparison stringComparisonType)
        {
            stringComparisonToUse = stringComparisonType;
        }
        #endregion

        /// <summary>
        /// For speed, a delegate is used when we know the type of value that will be returned
        /// from the GetValue() method
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public delegate int TypeSensitiveCompare(T x, T y);
        public TypeSensitiveCompare DoCompare;

        /// <summary>
        /// Sets up cached PropertyInfo and determines the best delegate to use to compare values
        /// retrieved from that property.
        /// </summary>
        public void Initialize()
        {
            if (fFoundProperty == false)
            {
                fFoundProperty = true;
                if (pi == null)
                {
                    PropertyDescriptorCollection props = TypeDescriptor.GetProperties(typeof(T));
                    property = props[sPropertyName];
                    pi = typeof(T).GetProperty(sPropertyName);

                    if (pi == null)
                    {
                        throw new Exception("Property name " + sPropertyName +
                          " not found while trying to compare objects of type " + typeof(T).Name);
                    }
                }
                typ = pi.PropertyType;
                // Set up the property comparison delegate to use based on the type of values we will be comparing
                if (sortType == SortType.eUsePropertyOrFieldType)
                {
                    sortType = Sorting.GetSortTypeEnumForType(typ);
                    if (typ == typeof(string))
                    {
                        if (stringComparisonToUse == StringComparison.Ordinal) DoCompare = StringCompareOrdinal;
                        else DoCompare = StringCompare;
                    }
                    else if (typ == typeof(int) && !fSortDescending) DoCompare = CompareInt;
                    else if (typ == typeof(int)) DoCompare = CompareIntDesc;
                    else if (typ == typeof(DateTime)) DoCompare = CompareDates;
                    else if (typ == typeof(long)) DoCompare = CompareTypeSensitive<long>;
                    else if (typ == typeof(double)) DoCompare = CompareTypeSensitive<double>;
                    else if (typ == typeof(float)) DoCompare = CompareTypeSensitive<float>;
                    else if (typ == typeof(short)) DoCompare = CompareTypeSensitive<short>;
                    else if (typ == typeof(byte)) DoCompare = CompareTypeSensitive<byte>;
                    else if (typ == typeof(bool)) DoCompare = CompareTypeSensitive<bool>;
                    else if (typ.BaseType == typeof(Enum))
                    {
                        FastEnumLookup = new Dictionary<int, string>(32);
                        if (fSortDescending) { DoCompare = FastCompareEnumsDesc; }
                        else { DoCompare = FastCompareEnumsAsc; }
                    }
                    else DoCompare = CompareUsingToString;
                }
                else
                {
                    if (sortType == SortType.eString) DoCompare = CompareUsingToString;
                    else if (sortType == SortType.eByte) DoCompare = CompareUsingToByte;
                    else if (sortType == SortType.eDateTime) DoCompare = CompareUsingToDate;
                    else if (sortType == SortType.eInteger) DoCompare = CompareUsingToInt;
                    else if (sortType == SortType.eLong) DoCompare = CompareUsingToInt64;
                    else if (sortType == SortType.eDoubleOrFloat) DoCompare = CompareUsingToDouble;
                    else DoCompare = CompareUsingToString;
                }
            }
        }

        #region "Compare method - handles value retrieval and value comparison"
        /// <summary>
        /// Compare to values, both of which are known to be of type string
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public int StringCompare(T x, T y)
        {
            //int nComp = string.Compare((string)pi.GetValue(x, null), (string)pi.GetValue(y, null), stringComparisonToUse);
            int nComp = string.Compare((string)property.GetValue(x), (string)property.GetValue(y), stringComparisonToUse);
            return (!fSortDescending ? nComp : -nComp);
        }

        public int StringCompareOrdinal(T x, T y)
        {
            //int nComp = string.Compare((string)pi.GetValue(x, null), (string)pi.GetValue(y, null), StringComparison.Ordinal);
            int nComp = string.Compare((string)property.GetValue(x), (string)property.GetValue(y), StringComparison.Ordinal);
            return (!fSortDescending ? nComp : -nComp);
        }

        public int CompareIntDesc(T x, T y)
        {
            //int oX = (int)pi.GetValue(x, null);
            //int oY = (int)pi.GetValue(y, null);
            int oX = (int)property.GetValue(x);
            int oY = (int)property.GetValue(y);
            return (oX < oY) ? 1 : ((oX == oY) ? 0 : -1);
        }

        public int CompareInt(T x, T y)
        {
            //int oX = (int)pi.GetValue(x, null);
            //int oY = (int)pi.GetValue(y, null);
            int oX = (int)property.GetValue(x);
            int oY = (int)property.GetValue(y);
            return (oX < oY) ? -1 : ((oX == oY) ? 0 : 1);
        }

        /// <summary>
        /// Compare to values, checking for null and converting to strings before comparison
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public int CompareUsingToString(T x, T y)
        {
            //object oX = pi.GetValue(x, null);
            //object oY = pi.GetValue(y, null);
            object oX = property.GetValue(x);
            object oY = property.GetValue(y);
            int nComp;
            // handle null appropriately only for string sorting
            if (oX == null) { nComp = (oY != null) ? -1 : 0; }
            else if (oY == null) { nComp = 1; }
            else { nComp = string.Compare(oX.ToString(), oY.ToString(), stringComparisonToUse); }
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareUsingToByte(T x, T y)
        {
            //byte oX = Convert.ToByte(pi.GetValue(x, null));
            //byte oY = Convert.ToByte(pi.GetValue(y, null));
            byte oX = Convert.ToByte(property.GetValue(x));
            byte oY = Convert.ToByte(property.GetValue(y));
            int nComp = (oX > oY) ? 1 : ((oX < oY) ? -1 : 0);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareUsingToInt(T x, T y)
        {
            //int oX = Convert.ToInt32(pi.GetValue(x, null));
            //int oY = Convert.ToInt32(pi.GetValue(y, null));
            int oX = Convert.ToInt32(property.GetValue(x));
            int oY = Convert.ToInt32(property.GetValue(y));
            int nComp = (oX > oY) ? 1 : ((oX < oY) ? -1 : 0);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareUsingToInt64(T x, T y)
        {
            //Int64 oX = Convert.ToInt64(pi.GetValue(x, null));
            //Int64 oY = Convert.ToInt64(pi.GetValue(y, null));
            Int64 oX = Convert.ToInt64(property.GetValue(x));
            Int64 oY = Convert.ToInt64(property.GetValue(y));
            int nComp = (oX > oY) ? 1 : ((oX < oY) ? -1 : 0);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareUsingToDouble(T x, T y)
        {
            //double oX = Convert.ToDouble(pi.GetValue(x, null));
            //double oY = Convert.ToDouble(pi.GetValue(y, null));
            double oX = Convert.ToDouble(property.GetValue(x));
            double oY = Convert.ToDouble(property.GetValue(y));
            int nComp = (oX > oY) ? 1 : ((oX < oY) ? -1 : 0);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareUsingToDate(T x, T y)
        {
            //DateTime oX = Convert.ToDateTime(pi.GetValue(x, null));
            //DateTime oY = Convert.ToDateTime(pi.GetValue(y, null));
            DateTime oX = Convert.ToDateTime(property.GetValue(x));
            DateTime oY = Convert.ToDateTime(property.GetValue(y));
            int nComp = oX.CompareTo(oY);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareDates(T x, T y)
        {
            //DateTime oX = (DateTime)pi.GetValue(x, null);
            //DateTime oY = (DateTime)pi.GetValue(y, null);
            DateTime oX = (DateTime)property.GetValue(x);
            DateTime oY = (DateTime)property.GetValue(y);
            int nComp = oX.CompareTo(oY);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareTypeSensitive<T2>(T x, T y) where T2 : IComparable<T2>
        {
            //T2 oX = (T2)pi.GetValue(x, null);
            //T2 oY = (T2)pi.GetValue(y, null);
            T2 oX = (T2)property.GetValue(x);
            T2 oY = (T2)property.GetValue(y);
            int nComp = oX.CompareTo(oY);
            return (!fSortDescending) ? nComp : -nComp;
        }

        /// <summary>
        /// Faster than comparing enums using .ToString() 
        /// (the Enum.ToString() method appears to be fairly expensive)
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public int FastCompareEnumsAsc(T x, T y)
        {            
            int oX = (int)property.GetValue(x);
            int oY = (int)property.GetValue(y);
            string s1, s2;

            if (!FastEnumLookup.TryGetValue(oX, out s1))
            {
                Enum eX = (Enum)property.GetValue(x);
                s1 = eX.ToString();
                FastEnumLookup.Add(oX, s1);
            }
            if (!FastEnumLookup.TryGetValue(oY, out s2))
            {
                Enum eY = (Enum)property.GetValue(y);
                s2 = eY.ToString();
                FastEnumLookup.Add(oY, s2);
            }
            return s1.CompareTo(s2);
        }

        public int FastCompareEnumsDesc(T x, T y)
        {
            int oX = (int)property.GetValue(x);
            int oY = (int)property.GetValue(y);
            string s1, s2;

            if (!FastEnumLookup.TryGetValue(oX, out s1))
            {
                Enum eX = (Enum)property.GetValue(x);
                s1 = eX.ToString();
                FastEnumLookup.Add(oX, s1);
            }
            if (!FastEnumLookup.TryGetValue(oY, out s2))
            {
                Enum eY = (Enum)property.GetValue(y);
                s2 = eY.ToString();
                FastEnumLookup.Add(oY, s2);
            }
            return s2.CompareTo(s1);
        }
        #endregion

        int IComparer<T>.Compare(T x, T y) { return DoCompare(x, y); }
    }

    /// <summary>
    /// Provides (fairly) quick access to field values and handles value comparisons. 
    /// Note: This class uses FieldInfo.GetValue to retrieve field values.  Speed of field value retrieval is significantly
    /// slower when retrieving the value of protected or private fields (due to a runtime security check when retrieving
    /// protected/private field values).  Also, due to the massive speed improvements in retrieving property values offered 
    /// by Marc Gravell in his article at http://www.codeproject.com/csharp/HyperPropertyDescriptor.asp, 
    /// property value retrieval is currently much faster than retrieving the value of a field.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class CompareFields<T> : IComparer<T> where T : class
    {
        private static Dictionary<int, string> FastEnumLookup;

        public SortType sortType = SortType.eUsePropertyOrFieldType;

        #region "Internal Fields"
        /// <summary>
        /// Stores the cached field info and type, used to retrieve field values
        /// Note: Retrieving field values is moderately to significantly faster than 
        /// retrieving property values (even if the property's Get accessor only returns 
        /// the underlying field value).  However, you should make sure that you
        /// aren't bypassing critical logic, error checking, or code safety by directly
        /// accessing field values.
        /// </summary>    
        public FieldInfo fi;
        internal Type typ;
        internal bool fFoundField;
        internal string sFieldName;
        internal bool fSortDescending;
        internal StringComparison stringComparisonToUse = StringComparison.Ordinal;
        #endregion

        #region "Constructors"
        public CompareFields(string sNameOfField, bool fDescendingSort)
        {
            sFieldName = sNameOfField;
            fSortDescending = fDescendingSort;
        }

        public CompareFields(string sNameOfField, bool fDescendingSort, SortType sortTyp)
        {
            sFieldName = sNameOfField;
            fSortDescending = fDescendingSort;
            sortType = sortTyp;
        }
        #endregion

        #region "Manual Overrides"
        /// <summary>
        /// Override the sort type at your own peril.  If any field/property value can't be converted
        /// to the type you specify, an exception will be raised.
        /// </summary>
        /// <param name="sortTyp"></param>
        public void SetOverrideSortType(SortType sortTyp) { sortType = sortTyp; }

        /// <summary>
        /// Default string comparison type is Ordinal.  Using this method, you can specify
        /// other options, such as OrdinalIgnoreCase (for case insensitive comparison), etc
        /// </summary>
        /// <param name="stringComparisonType"></param>
        public void SetStringComparisonType(StringComparison stringComparisonType)
        {
            stringComparisonToUse = stringComparisonType;
        }
        #endregion

        /// <summary>
        /// For speed, a delegate is used when we know the type of value that will be returned
        /// from the GetValue() method
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public delegate int TypeSensitiveCompare(T x, T y);
        public TypeSensitiveCompare DoCompare;

        /// <summary>
        /// Sets up cached FieldInfo and determines the best delegate to use to compare values
        /// retrieved from that field.
        /// </summary>
        public void Initialize()
        {
            if (fFoundField == false)
            {
                fFoundField = true;

                if (fi == null)
                {
                    // You can play around with binding flags if you really want to access nonpublic fields, etc... 
                    // note that there is a significant performance hit on accessing protected and private fields,
                    // since security / permissions are checked every time, from what I can tell.  It's better
                    // just to go through public properties if you're not accessing public fields.
                    // fi = typeof(T).GetField(sFieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
                    fi = typeof(T).GetField(sFieldName);
                    if (fi == null)
                    {
                        throw new Exception("Field name " + sFieldName +
                          " not found while trying to compare objects of type " + typeof(T).Name);
                    }
                }
                typ = fi.FieldType;
                if (sortType == SortType.eUsePropertyOrFieldType)
                {
                    sortType = Sorting.GetSortTypeEnumForType(typ);
                    if (typ == typeof(string))
                    {
                        if (stringComparisonToUse == StringComparison.Ordinal) DoCompare = StringCompareOrdinal;
                        else DoCompare = StringCompare;
                    }
                    else if (typ == typeof(int) && !fSortDescending) DoCompare = CompareInt;
                    else if (typ == typeof(int)) DoCompare = CompareIntDesc;
                    else if (typ == typeof(DateTime)) DoCompare = CompareDates;
                    else if (typ == typeof(long)) DoCompare = CompareTypeSensitive<long>;
                    else if (typ == typeof(double)) DoCompare = CompareTypeSensitive<double>;
                    else if (typ == typeof(float)) DoCompare = CompareTypeSensitive<float>;
                    else if (typ == typeof(short)) DoCompare = CompareTypeSensitive<short>;
                    else if (typ == typeof(byte)) DoCompare = CompareTypeSensitive<byte>;
                    else if (typ == typeof(bool)) DoCompare = CompareTypeSensitive<bool>;
                    else if (typ.BaseType == typeof(Enum))
                    {
                        FastEnumLookup = new Dictionary<int, string>(32);
                        if (fSortDescending) { DoCompare = FastCompareEnumsDesc; }
                        else { DoCompare = FastCompareEnumsAsc; }
                    }
                    else DoCompare = CompareUsingToString;
                    // optimize to use the ABOVE path if the property or field type matches
                    // the requested sort type (i.e. below)
                }
                else
                {
                    if (sortType == SortType.eString) DoCompare = CompareUsingToString;
                    else if (sortType == SortType.eByte) DoCompare = CompareUsingToByte;
                    else if (sortType == SortType.eDateTime) DoCompare = CompareUsingToDate;
                    else if (sortType == SortType.eInteger) DoCompare = CompareUsingToInt;
                    else if (sortType == SortType.eLong) DoCompare = CompareUsingToInt64;
                    else if (sortType == SortType.eDoubleOrFloat) DoCompare = CompareUsingToDouble;
                    else DoCompare = CompareUsingToString;
                }
            }
        }

        #region "Compare method - handles retrieval and comparison"
        public int StringCompare(T x, T y)
        {
            int nComp = string.Compare((string)fi.GetValue(x), (string)fi.GetValue(y), stringComparisonToUse);
            return (!fSortDescending ? nComp : -nComp);
        }

        public int StringCompareOrdinal(T x, T y)
        {
            int nComp = string.Compare((string)fi.GetValue(x), (string)fi.GetValue(y), StringComparison.Ordinal);
            return (!fSortDescending ? nComp : -nComp);
        }

        public int CompareIntDesc(T x, T y)
        {
            int oX = (int)fi.GetValue(x);
            int oY = (int)fi.GetValue(y);
            return (oX < oY) ? 1 : ((oX == oY) ? 0 : -1);
        }
        public int CompareInt(T x, T y)
        {
            int oX = (int)fi.GetValue(x);
            int oY = (int)fi.GetValue(y);
            return (oX < oY) ? -1 : ((oX == oY) ? 0 : 1);
        }

        public int CompareUsingToString(T x, T y)
        {
            object oX = fi.GetValue(x);
            object oY = fi.GetValue(y);
            int nComp;
            // handle null appropriately only for string sorting
            if (oX == null) { nComp = (oY != null) ? -1 : 0; }
            else if (oY == null) { nComp = 1; }
            else { nComp = string.Compare(oX.ToString(), oY.ToString(), stringComparisonToUse); }
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareUsingToByte(T x, T y)
        {
            byte oX = Convert.ToByte(fi.GetValue(x));
            byte oY = Convert.ToByte(fi.GetValue(y));
            int nComp = (oX > oY) ? 1 : ((oX < oY) ? -1 : 0);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareUsingToInt(T x, T y)
        {
            int oX = Convert.ToInt32(fi.GetValue(x));
            int oY = Convert.ToInt32(fi.GetValue(y));
            int nComp = (oX > oY) ? 1 : ((oX < oY) ? -1 : 0);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareUsingToInt64(T x, T y)
        {
            Int64 oX = Convert.ToInt64(fi.GetValue(x));
            Int64 oY = Convert.ToInt64(fi.GetValue(y));
            int nComp = (oX > oY) ? 1 : ((oX < oY) ? -1 : 0);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareUsingToDouble(T x, T y)
        {
            double oX = Convert.ToDouble(fi.GetValue(x));
            double oY = Convert.ToDouble(fi.GetValue(y));
            int nComp = (oX > oY) ? 1 : ((oX < oY) ? -1 : 0);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareUsingToDate(T x, T y)
        {
            DateTime oX = Convert.ToDateTime(fi.GetValue(x));
            DateTime oY = Convert.ToDateTime(fi.GetValue(y));
            int nComp = oX.CompareTo(oY);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareDates(T x, T y)
        {
            DateTime oX = (DateTime)fi.GetValue(x);
            DateTime oY = (DateTime)fi.GetValue(y);
            int nComp = oX.CompareTo(oY);
            return (!fSortDescending) ? nComp : -nComp;
        }

        public int CompareTypeSensitive<T2>(T x, T y) where T2 : IComparable<T2>
        {
            T2 oX = (T2)fi.GetValue(x);
            T2 oY = (T2)fi.GetValue(y);
            int nComp = oX.CompareTo(oY);
            return (!fSortDescending) ? nComp : -nComp;
        }

        /// <summary>
        /// Faster than comparing enums using .ToString() 
        /// (the Enum.ToString() method appears to be fairly expensive)
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public int FastCompareEnumsAsc(T x, T y)
        {
            int oX = (int)fi.GetValue(x);
            int oY = (int)fi.GetValue(y);
            string s1, s2;

            if (!FastEnumLookup.TryGetValue(oX, out s1))
            {
                Enum eX = (Enum)fi.GetValue(x);
                s1 = eX.ToString();
                FastEnumLookup.Add(oX, s1);
            }
            if (!FastEnumLookup.TryGetValue(oY, out s2))
            {
                Enum eY = (Enum)fi.GetValue(y);
                s2 = eY.ToString();
                FastEnumLookup.Add(oY, s2);
            }
            return s1.CompareTo(s2);
        }

        public int FastCompareEnumsDesc(T x, T y)
        {
            int oX = (int)fi.GetValue(x);
            int oY = (int)fi.GetValue(y);
            string s1, s2;

            if (!FastEnumLookup.TryGetValue(oX, out s1))
            {
                Enum eX = (Enum)fi.GetValue(x);
                s1 = eX.ToString();
                FastEnumLookup.Add(oX, s1);
            }
            if (!FastEnumLookup.TryGetValue(oY, out s2))
            {
                Enum eY = (Enum)fi.GetValue(y);
                s2 = eY.ToString();
                FastEnumLookup.Add(oY, s2);
            }
            return s2.CompareTo(s1);
        }
        #endregion

        int IComparer<T>.Compare(T x, T y) { return DoCompare(x, y); }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Software Developer (Senior) Troppus Software
United States United States
Currently working as a Senior Silverlight Developer with Troppus Software in Superior, CO. I enjoy statistics, programming, new technology, playing the cello, and reading codeproject articles. Smile | :)

Comments and Discussions