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

Automatic Expandable Properties in a PropertyGrid

Rate me:
Please Sign up or sign in to vote.
2.00/5 (3 votes)
4 Jan 20064 min read 71.7K   2K   26  
The article describes a family of classes that will automatically enable each of your custom-type's public properties to be expandable in a PropertyGrid without the need to write an explicit TypeConverter. You will also be able to edit those properties that support the 'set' accessor.
///////////////////////////////////////////////////////////////////////////////////////////////
/// NOTE THAT: The basic architecture and the cache-mechanism used in ExpandableObjects
///  are lifted from Stephen Taub's article in NET Matters April/May issues, 
/// 
/// NET Matters ICustomTypeDescriptor, Part 1 -- MSDN Magazine, April 2005
///		http://msdn.microsoft.com/msdnmag/issues/05/04/NETMatters/default.aspx
///	NET Matters ICustomTypeDescriptor, Part 2 -- MSDN Magazine, May 2005
///		http://msdn.microsoft.com/msdnmag/issues/05/05/NETMatters/default.aspx
///
/// My thanks to the author for the information/techniques covered therein and permission to
/// reuse code for non-profit purposes.
////////////////////////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
using System.Diagnostics;

namespace ExpandableObjects
{
	public abstract class ExpandableObject : IDisposable
	{
		#region Fields
		private ExpandablePropertiesTypeDescriptionProvider _provider = null;
		#endregion

		#region WAYOUT
		~ExpandableObject()
		{
			Dispose(false);
		}

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Ensure that subclasses call this implementation of Dispose.  Failing this, they MUST call:
		///	TypeDescriptor.RemoveProvider to remove the ExpandablePropertiesTypeDescriptionProvider for this instance
		/// </summary>
		/// <param name="bDisposing"></param>
		protected virtual void Dispose(bool bDisposing)
		{
			if(bDisposing){}
			if(_provider != null)
				TypeDescriptor.RemoveProvider(_provider, this);
			_provider = null;
		}
		#endregion

		#region CTOR
		/// <summary>
		/// Construct the base-class object and register it's own ExpandablePropertiesTypeDescriptionProvider which
		/// will return (where applicable) an ExpandableObjectConverter whenever a TypeConverter is requested for 
		/// the instance.
		/// Note that this is an INSTANCE-LEVEL extender.
		/// </summary>
		protected ExpandableObject()
		{
			_provider = new ExpandablePropertiesTypeDescriptionProvider(GetType());
			TypeDescriptor.AddProvider(_provider, this);
		}
		#endregion
	}

	public class DefaultPropertyDescriptor : PropertyDescriptor
	{
		#region The Described Property
		private PropertyInfo _prop;
		public PropertyInfo Prop { get { return _prop; } }
		#endregion

		#region Read-Only / Read-Write
		public override bool IsReadOnly
		{
			get
			{
				if(this.Prop.GetSetMethod() == null)
					return true;
				return false;
			} 
		}
		#endregion

		#region PropertyDescriptor pass-thrus
		public override void ResetValue(object component) { }
		public override bool CanResetValue(object component) { return false; }
		public override bool ShouldSerializeValue(object component) { return true; }
		public override Type ComponentType { get { return _prop.DeclaringType; } }
		public override Type PropertyType { get { return _prop.PropertyType; } }
		#endregion

		#region CTOR
		public DefaultPropertyDescriptor(PropertyInfo prop) : base(prop.Name, (Attribute[])prop.GetCustomAttributes(typeof(Attribute), true))
		{
			_prop = prop;
		}
		#endregion

		#region GetValue/SetValue
		public override object GetValue(object component)
		{
			return _prop.GetValue(component, null);
		}
		public override void SetValue(object component, object value)
		{
			_prop.SetValue(component, value, null);
			OnValueChanged(component, EventArgs.Empty);
		}
		
		#endregion

		#region Equality
		public override int GetHashCode() { return _prop.GetHashCode(); }
		public override bool Equals(object obj)
		{
			DefaultPropertyDescriptor other = obj as DefaultPropertyDescriptor;
			return other != null && other._prop.Equals(_prop);
		}
		#endregion

	}

	/// <summary>
	/// This class is a mofified version of a class from Stephen Taub's NET Matters, ICustomTypeDescriptor article in NET Mattter April/May 2005 
	/// </summary>
	public class ExpandablePropertiesTypeDescriptionProvider : TypeDescriptionProvider
	{
		#region Fields
		private TypeDescriptionProvider _baseProvider;
		private PropertyDescriptorCollection _propCache;
		private FilterCache _filterCache;
		#endregion

		#region CTOR
		public ExpandablePropertiesTypeDescriptionProvider(Type t)
		{
			_baseProvider = TypeDescriptor.GetProvider(t);
		}
		#endregion

		#region GET TYPE DESCRIPTOR
		public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
		{
			return new ExpandablePropertiesTypeDescriptor(this, _baseProvider.GetTypeDescriptor(objectType, instance), objectType);
		}
		#endregion

		#region FilterCache
		/// <summary>
		/// This class is a direct lift from Stephen Taub's NET Matters, ICustomTypeDescriptor article in NET Mattter April/May 2005
		/// </summary>
		private class FilterCache
		{
			public Attribute[] Attributes;
			public PropertyDescriptorCollection FilteredProperties;
			/// <summary>
			/// Perform straight-match on attributes: non-null, same-length, same contents
			/// </summary>
			/// <param name="other"></param>
			/// <returns></returns>
			public bool IsValid(Attribute[] other)
			{
				if (other == null || Attributes == null) return false;
				if (Attributes.Length != other.Length) return false;
				for (int i = 0; i < other.Length; i++)
				{
					if (!Attributes[i].Match(other[i])) return false;
				}
				return true;
			}
		}
		#endregion

		#region Dialgnostics
		[Conditional("DEBUG")]
		private static void DumpAttributes(PropertyInfo p, Attribute[] a)
		{
			string accessMods = string.Format("\nACCESS: {0}", p.PropertyType.IsPublic ? "public" : "private");
			Debug.WriteLine("DUMPING ATTRIBUTES for: " + p.Name + accessMods);
			foreach (Attribute aa in a)
				Debug.WriteLine(aa);
		}
		#endregion

		#region ExpandabPropertyDescriptor
		public class ExpandabPropertyDescriptor : DefaultPropertyDescriptor
		{
			#region CTOR
			public ExpandabPropertyDescriptor(PropertyInfo prop) : base(prop)
			{ }
			#endregion

			#region REPLACE TypeConverter with ExpandableObjectConverter
			private TypeConverter conv = null;
			public override TypeConverter Converter
			{
				get
				{
					if (conv == null)
						if (base.Converter.GetType() != typeof(TypeConverter))
						{
							// if there is a custom-converter already - return it
							conv = base.Converter;
						}
						else // force an expandable-converter for this property
							conv = new ExpandableObjectConverter();

					return conv;

				}
			}
			#endregion
		}

		#endregion

		#region ExpandablePropertiesTypeDescriptor
		/// <summary>
		/// This class is a modified version of a class from Stephen Taub's NET Matters, ICustomTypeDescriptor article: 
		/// </summary>
		private class ExpandablePropertiesTypeDescriptor : CustomTypeDescriptor
		{
			private Type _objectType;
			private ExpandablePropertiesTypeDescriptionProvider _provider;

			public ExpandablePropertiesTypeDescriptor(ExpandablePropertiesTypeDescriptionProvider provider, ICustomTypeDescriptor descriptor, Type objectType)
				: base(descriptor)
			{
				if (provider == null) throw new ArgumentNullException("provider");
				if (descriptor == null) throw new ArgumentNullException("descriptor");
				if (objectType == null) throw new ArgumentNullException("objectType");
				_objectType = objectType;
				_provider = provider;
			}

			public override PropertyDescriptorCollection GetProperties()
			{
				return GetProperties(null);
			}

			public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
			{
				// Retrieve cached properties and filtered properties
				bool filtering = attributes != null && attributes.Length > 0;
				FilterCache cache = _provider._filterCache;
				PropertyDescriptorCollection props = _provider._propCache;

				// Use a cached version if we can
				if (filtering && cache != null && cache.IsValid(attributes))
					return cache.FilteredProperties;
				else if (!filtering && props != null)
					return props;

				// Otherwise, create the property collection
				props = new PropertyDescriptorCollection(null);
				foreach (PropertyInfo p in _objectType.GetProperties())
				{
					//	FieldInfo[] pflds = p.PropertyType.GetFields();
					PropertyInfo[] pprops = p.PropertyType.GetProperties();
					PropertyDescriptor desc = null;

					//// if the property in not an array and has public fields or properties - use ExpandablePropertyDescriptor
					if (!p.PropertyType.HasElementType && ((pprops.Length > 0)))
						desc = new ExpandabPropertyDescriptor(p);
					else
						desc = new DefaultPropertyDescriptor(p);

					if (!filtering || desc.Attributes.Contains(attributes))
						props.Add(desc);
				}

				// Store the updated properties
				if (filtering)
				{
					cache = new FilterCache();
					cache.FilteredProperties = props;
					cache.Attributes = attributes;
					_provider._filterCache = cache;
				}
				else _provider._propCache = props;

				// Return the computed properties
				return props;
			}
		}
		#endregion

	}

}

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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United Kingdom United Kingdom
The whole world's a circus... don't you be the clown

Comments and Discussions