After hours of searching and messing around, I found a solution - and a beautiful one in that - from the Jared Par blog . It creates a proxy type that uses reflection to compact all members into a single list. Some of the additional magic of DebuggerDisplay does this so that you donβt even notice.
// http://blogs.msdn.com/b/jaredpar/archive/2010/02/19/flattening-class-hierarchies-when-debugging-c.aspx // by Jared Par internal sealed class FlattenHierarchyProxy { [DebuggerDisplay("{Value}", Name = "{Name,nq}", Type = "{Type.ToString(),nq}")] internal struct Member { internal string Name; internal object Value; internal Type Type; internal Member(string name, object value, Type type) { Name = name; Value = value; Type = type; } } [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly object _target; [DebuggerBrowsable(DebuggerBrowsableState.Never)] private Member[] _memberList; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] internal Member[] Items { get { if (_memberList == null) { _memberList = BuildMemberList().ToArray(); } return _memberList; } } public FlattenHierarchyProxy(object target) { _target = target; } private List<Member> BuildMemberList() { var list = new List<Member>(); if ( _target == null ) { return list; } var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var type = _target.GetType(); foreach (var field in type.GetFields(flags)) { var value = field.GetValue(_target); list.Add(new Member(field.Name, value, field.FieldType)); } foreach (var prop in type.GetProperties(flags)) { object value = null; try { value = prop.GetValue(_target, null); } catch (Exception ex) { value = ex; } list.Add(new Member(prop.Name, value, prop.PropertyType)); } return list; } }
<h / "> Modifications
I made three small modifications to the class to make it more convenient for me.
At first, I wanted the participants to sort by name. To do this, change the last line to:
return list.OrderBy(m => m.Name).ToList();
Secondly, in the Member structure, I added some attributes so that when expanding the reference class, only the value is displayed:
[DebuggerBrowsable(DebuggerBrowsableState.Never)] internal string Name; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] internal object Value; [DebuggerBrowsable(DebuggerBrowsableState.Never)] internal Type Type;
Third, the default flags are BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance means that even items marked with [DebuggerBrowsable(DebuggerBrowsableState.Never)] will be displayed. To prevent this from happening, after this line:
foreach (var field in type.GetFields(flags)) {
add this:
// Respect DebuggerBrowsableAttributes var debuggerBrowsableAtts = field.GetCustomAttributes(typeof(DebuggerBrowsableAttribute), true); if (debuggerBrowsableAtts.Count() == 1) { var att = debuggerBrowsableAtts[0] as DebuggerBrowsableAttribute; if (att.State == DebuggerBrowsableState.Never) { continue; } }
Now DebuggerBrowsable(DebuggerBrowsableState.Never) will be respected for fields. You can also add this code to a foreach loop that processes properties so that it also takes properties into account.