I'm building a custom property grid that displays the properties of items in a collection. What I want to do is show only the properties in the grid that are common amongst each item. I am assuming the best way to do this would be to find the the common base class of each type in the collection and display it's properties. Is there any easier way? Can you give me a code example of the best approach to do this?
Well,
You could create in interface similar to IComparable but instead call it something like IPropertyComparable and then have the classes that implement it use reflection to compare their property names as so...
public int Compare(T x, T y)
{
PropertyInfo[] props = x.GetType().GetProperties();
foreach(PropertyInfo info in props)
{
if(info.name == y.GetType().Name)
....
}
...
I'll let you figure out the rest. It could probably be a little more elegant anyway, use LINQ maybe...
- Matt
You can do this with a method that keeps checking for common base classes. I wrote up this, quickly, using the BaseClass feature of the Type class. You don't have to use an array, a list or other IEnumerable can work with small modifications to this.
I tested it with:
static void Main(string[] args)
{
Console.WriteLine("Common Types: " + GetCommonBaseClass(new Type[] {typeof(OleDbCommand), typeof(OdbcCommand), typeof(SqlCommand)}).ToString());
}
And got the right answer of DbCommand. Here is my code.
static Type GetCommonBaseClass(Type[] types)
{
if (types.Length == 0)
return (typeof(object));
else if (types.Length == 1)
return (types[0]);
// Copy the parameter so we can substitute base class types in the array without messing up the caller
Type[] temp = new Type[types.Length];
for (int i = 0; i < types.Length; i++)
{
temp[i] = types[i];
}
bool checkPass = false;
Type tested = null;
while (!checkPass)
{
tested = temp[0];
checkPass = true;
for (int i = 1; i < temp.Length; i++)
{
if (tested.Equals(temp[i]))
continue;
else
{
// If the tested common basetype (current) is the indexed type's base type
// then we can continue with the test by making the indexed type to be its base type
if (tested.Equals(temp[i].BaseType))
{
temp[i] = temp[i].BaseType;
continue;
}
// If the tested type is the indexed type's base type, then we need to change all indexed types
// before the current type (which are all identical) to be that base type and restart this loop
else if (tested.BaseType.Equals(temp[i]))
{
for (int j = 0; j <= i - 1; j++)
{
temp[j] = temp[j].BaseType;
}
checkPass = false;
break;
}
// The indexed type and the tested type are not related
// So make everything from index 0 up to and including the current indexed type to be their base type
// because the common base type must be further back
else
{
for (int j = 0; j <= i; j++)
{
temp[j] = temp[j].BaseType;
}
checkPass = false;
break;
}
}
}
// If execution has reached here and checkPass is true, we have found our common base type,
// if checkPass is false, the process starts over with the modified types
}
// There's always at least object
return tested;
}
Here is a way to get the common set of properties from a list of types:
class TypeHandler
{
public static List<string> GetCommonProperties(Type[] types)
{
Dictionary<string, int> propertyCounts = new Dictionary<string, int>();
foreach (Type type in types)
{
foreach (PropertyInfo info in type.GetProperties())
{
string name = info.Name;
if (!propertyCounts.ContainsKey(name)) propertyCounts.Add(name, 0);
propertyCounts[name]++;
}
}
List<string> propertyNames = new List<string>();
foreach (string name in propertyCounts.Keys)
{
if (propertyCounts[name] == types.Length) propertyNames.Add(name);
}
return propertyNames;
}
}
This iterates over all the properties in all the types and only ends up with those occurring a number of times equal to the number of types.
If you prefer compact LINQ queries you can use the following equivalent expression:
return (from t in types
from p in t.GetProperties()
group p by p.Name into pg
where pg.Count() == types.Length
select pg.Key).ToList();
To get the common properties from a collection of Objects, you can use a method like this:
public static String[] GetCommonPropertiesByName(Object[] objs)
{
List<Type> typeList = new List<Type>(Type.GetTypeArray(objs));
List<String> propertyList = new List<String>();
List<String> individualPropertyList = new List<String>();
foreach (Type type in typeList)
{
foreach (PropertyInfo property in type.GetProperties())
{
propertyList.Add(property.Name);
}
}
propertyList = propertyList.Distinct().ToList();
foreach (Type type in typeList)
{
individualPropertyList.Clear();
foreach (PropertyInfo property in type.GetProperties())
{
individualPropertyList.Add(property.Name);
}
propertyList = propertyList.Intersect(individualPropertyList).ToList();
}
return propertyList.ToArray();
}
Then, once you have the string of a property you want to do something with, you can take any of the Objects in the collection and use reflection to call that Property by its String Name.
PropertyInfo p = t.GetType().GetProperty("some Property String Name");
p.GetValue(t, null);
p.SetValue(t, someNewValue, null);
Similarly, the code in the GetCommonPropertiesByName
method can be modified to get common members, methods, nested types, fields, etc...
I use something like this but Tony's answer is probably better:
internal class BaseFinder
{
public static Type FindBase(params Type[] types)
{
if (types == null)
return null;
if (types.Length == 0)
return null;
Dictionary<Type, IList<Type>> baseTypeMap = new Dictionary<Type,IList<Type>>();
// get all the base types and note the one with the longest base tree
int maxBaseCount = 0;
Type typeWithLongestBaseTree = null;
foreach (Type type in types)
{
IList<Type> baseTypes = GetBaseTree(type);
if (baseTypes.Count > maxBaseCount)
{
typeWithLongestBaseTree = type;
maxBaseCount = baseTypes.Count;
}
baseTypeMap.Add(type, baseTypes);
}
// walk down the tree until we get to a common base type
IList<Type> longestBaseTree = baseTypeMap[typeWithLongestBaseTree];
for (int baseIndex = 0; baseIndex < longestBaseTree.Count;baseIndex++)
{
int commonBaseCount = 0;
foreach (Type type in types)
{
IList<Type> baseTypes = baseTypeMap[type];
if (!baseTypes.Contains(longestBaseTree[baseIndex]))
break;
commonBaseCount++;
}
if (commonBaseCount == types.Length)
return longestBaseTree[baseIndex];
}
return null;
}
private static IList<Type> GetBaseTree(Type type)
{
List<Type> result = new List<Type>();
Type baseType = type.BaseType;
do
{
result.Add(baseType);
baseType = baseType.BaseType;
} while (baseType != typeof(object));
return result;
}
}
The code posted to get the most-specific common base for a set of types has some issues. In particular, it breaks when I pass typeof(object) as one of the types. I believe the following is simpler and (better) correct.
public static Type GetCommonBaseClass (params Type[] types)
{
if (types.Length == 0)
return typeof(object);
Type ret = types[0];
for (int i = 1; i < types.Length; ++i)
{
if (types[i].IsAssignableFrom(ret))
ret = types[i];
else
{
// This will always terminate when ret == typeof(object)
while (!ret.IsAssignableFrom(types[i]))
ret = ret.BaseType;
}
}
return ret;
}
I also tested with:
Type t = GetCommonBaseClass(typeof(OleDbCommand),
typeof(OdbcCommand),
typeof(SqlCommand));
And got typeof(DbCommand)
. And with:
Type t = GetCommonBaseClass(typeof(OleDbCommand),
typeof(OdbcCommand),
typeof(SqlCommand),
typeof(Component));
And got typeof(Compoment)
. And with:
Type t = GetCommonBaseClass(typeof(OleDbCommand),
typeof(OdbcCommand),
typeof(SqlCommand),
typeof(Component),
typeof(Component).BaseType);
And got typeof(MarshalByRefObject)
. And with
Type t = GetCommonBaseClass(typeof(OleDbCommand),
typeof(OdbcCommand),
typeof(SqlCommand),
typeof(Component),
typeof(Component).BaseType,
typeof(int));
And got typeof(object)
.