Skip to content

Commit 34bf55c

Browse files
authored
Improve and unify debug views of dictionaries. (#92534)
* Improves and unifies debug views of dictionaries. The change alows generic and non-generic dictionaries to be displayed in the same way: with separate columns for keys and values with an ability to expand each column. Fixes #88736 * Fix the DebuggerView tests of dictionaries Included non-generic dictionaries in tests * Fix more DebuggerView tests * Included more types that implement IDictionary in the DebuggerView tests. * Improved the testing code to support classes with attributes declared by their base classes. * Fixed .Net Framework 4.8 build error by removing a dependency on the record c# feature. * Fixed tests remaining tests (outside of the System.Collections subset) * Fix DebugView.Tests build errors on .Net Framework * Update DebugView tests to expect different outcomes on .Net Framework .Net Framwork does not support recent improvements in the way the debugger displays a dictionary. Depending on the framwork used, the debugger view of generic dictionaries and ListDictionaryInternal are different. * Applied suggested changes and fixes * mostly code style changes * restored a rest for an empty HashSet. * fixed testing of the generic SortedList. * Minor improvents Renamed an internal method to match its new behavior and removed unnecessary init accessors.
1 parent 655b177 commit 34bf55c

File tree

13 files changed

+290
-108
lines changed

13 files changed

+290
-108
lines changed

src/libraries/Common/tests/System/Collections/DebugView.Tests.cs

Lines changed: 147 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.Collections.ObjectModel;
56
using System.Diagnostics;
67
using System.Linq;
78
using System.Reflection;
@@ -11,14 +12,96 @@ namespace System.Collections.Tests
1112
{
1213
public class DebugView_Tests
1314
{
14-
public static IEnumerable<object[]> TestDebuggerAttributes_Inputs()
15+
private static IEnumerable<object[]> TestDebuggerAttributes_GenericDictionaries()
16+
{
17+
yield return new object[] { new Dictionary<int, string>(), new KeyValuePair<string, string>[0] };
18+
yield return new object[] { new ReadOnlyDictionary<int, string>(new Dictionary<int, string>()), new KeyValuePair<string, string>[0] };
19+
yield return new object[] { new SortedDictionary<string, int>(), new KeyValuePair<string, string>[0] };
20+
yield return new object[] { new SortedList<int, string>(), new KeyValuePair<string, string>[0] };
21+
22+
yield return new object[] { new Dictionary<int, string>{{1, "One"}, {2, "Two"}},
23+
new KeyValuePair<string, string>[]
24+
{
25+
new ("[1]", "\"One\""),
26+
new ("[2]", "\"Two\""),
27+
}
28+
};
29+
yield return new object[] { new ReadOnlyDictionary<int,string>(new Dictionary<int, string>{{1, "One"}, {2, "Two"}}),
30+
new KeyValuePair<string, string>[]
31+
{
32+
new ("[1]", "\"One\""),
33+
new ("[2]", "\"Two\""),
34+
}
35+
};
36+
yield return new object[] { new SortedDictionary<string, int>{{"One", 1}, {"Two", 2}} ,
37+
new KeyValuePair<string, string>[]
38+
{
39+
new ("[\"One\"]", "1"),
40+
new ("[\"Two\"]", "2"),
41+
}
42+
};
43+
yield return new object[] { new SortedList<string, double> { { "One", 1.0 }, { "Two", 2.0 } },
44+
new KeyValuePair<string, string>[]
45+
{
46+
new ("[\"One\"]", "1"),
47+
new ("[\"Two\"]", "2"),
48+
}
49+
};
50+
}
51+
52+
private static IEnumerable<object[]> TestDebuggerAttributes_NonGenericDictionaries()
53+
{
54+
yield return new object[] { new Hashtable(), new KeyValuePair<string, string>[0] };
55+
yield return new object[] { Hashtable.Synchronized(new Hashtable()), new KeyValuePair<string, string>[0] };
56+
yield return new object[] { new SortedList(), new KeyValuePair<string, string>[0] };
57+
yield return new object[] { SortedList.Synchronized(new SortedList()), new KeyValuePair<string, string>[0] };
58+
59+
yield return new object[] { new Hashtable { { "a", 1 }, { "b", "B" } },
60+
new KeyValuePair<string, string>[]
61+
{
62+
new ("[\"a\"]", "1"),
63+
new ("[\"b\"]", "\"B\""),
64+
}
65+
};
66+
yield return new object[] { Hashtable.Synchronized(new Hashtable { { "a", 1 }, { "b", "B" } }),
67+
new KeyValuePair<string, string>[]
68+
{
69+
new ("[\"a\"]", "1"),
70+
new ("[\"b\"]", "\"B\""),
71+
}
72+
};
73+
yield return new object[] { new SortedList { { "a", 1 }, { "b", "B" } },
74+
new KeyValuePair<string, string>[]
75+
{
76+
new ("[\"a\"]", "1"),
77+
new ("[\"b\"]", "\"B\""),
78+
}
79+
};
80+
yield return new object[] { SortedList.Synchronized(new SortedList { { "a", 1 }, { "b", "B" } }),
81+
new KeyValuePair<string, string>[]
82+
{
83+
new ("[\"a\"]", "1"),
84+
new ("[\"b\"]", "\"B\""),
85+
}
86+
};
87+
#if !NETFRAMEWORK // ListDictionaryInternal in .Net Framework is not annotated with debugger attributes.
88+
yield return new object[] { new Exception().Data, new KeyValuePair<string, string>[0] };
89+
yield return new object[] { new Exception { Data = { { "a", 1 }, { "b", "B" } } }.Data,
90+
new KeyValuePair<string, string>[]
91+
{
92+
new ("[\"a\"]", "1"),
93+
new ("[\"b\"]", "\"B\""),
94+
}
95+
};
96+
#endif
97+
}
98+
99+
private static IEnumerable<object[]> TestDebuggerAttributes_ListInputs()
15100
{
16-
yield return new object[] { new Dictionary<int, string>() };
17101
yield return new object[] { new HashSet<string>() };
18102
yield return new object[] { new LinkedList<object>() };
19103
yield return new object[] { new List<int>() };
20104
yield return new object[] { new Queue<double>() };
21-
yield return new object[] { new SortedDictionary<string, int>() };
22105
yield return new object[] { new SortedList<int, string>() };
23106
yield return new object[] { new SortedSet<int>() };
24107
yield return new object[] { new Stack<object>() };
@@ -30,39 +113,83 @@ public static IEnumerable<object[]> TestDebuggerAttributes_Inputs()
30113
yield return new object[] { new SortedList<string, int>().Keys };
31114
yield return new object[] { new SortedList<float, long>().Values };
32115

33-
yield return new object[] { new Dictionary<int, string>{{1, "One"}, {2, "Two"}} };
34-
yield return new object[] { new HashSet<string>{"One", "Two"} };
116+
yield return new object[] { new HashSet<string> { "One", "Two" } };
35117

36-
LinkedList<object> linkedList = new LinkedList<object>();
118+
LinkedList<object> linkedList = new();
37119
linkedList.AddFirst(1);
38120
linkedList.AddLast(2);
39121
yield return new object[] { linkedList };
40-
yield return new object[] { new List<int>{1, 2} };
122+
yield return new object[] { new List<int> { 1, 2 } };
41123

42-
Queue<double> queue = new Queue<double>();
124+
Queue<double> queue = new();
43125
queue.Enqueue(1);
44126
queue.Enqueue(2);
45127
yield return new object[] { queue };
46-
yield return new object[] { new SortedDictionary<string, int>{{"One", 1}, {"Two", 2}} };
47-
yield return new object[] { new SortedList<int, string>{{1, "One"}, {2, "Two"}} };
48-
yield return new object[] { new SortedSet<int>{1, 2} };
128+
yield return new object[] { new SortedSet<int> { 1, 2 } };
49129

50-
var stack = new Stack<object>();
130+
Stack<object> stack = new();
51131
stack.Push(1);
52132
stack.Push(2);
53133
yield return new object[] { stack };
54134

55-
yield return new object[] { new Dictionary<double, float>{{1.0, 1.0f}, {2.0, 2.0f}}.Keys };
56-
yield return new object[] { new Dictionary<float, double>{{1.0f, 1.0}, {2.0f, 2.0}}.Values };
57-
yield return new object[] { new SortedDictionary<Guid, string>{{Guid.NewGuid(), "One"}, {Guid.NewGuid(), "Two"}}.Keys };
58-
yield return new object[] { new SortedDictionary<long, Guid>{{1L, Guid.NewGuid()}, {2L, Guid.NewGuid()}}.Values };
59-
yield return new object[] { new SortedList<string, int>{{"One", 1}, {"Two", 2}}.Keys };
60-
yield return new object[] { new SortedList<float, long>{{1f, 1L}, {2f, 2L}}.Values };
135+
yield return new object[] { new SortedList<string, int> { { "One", 1 }, { "Two", 2 } }.Keys };
136+
yield return new object[] { new SortedList<float, long> { { 1f, 1L }, { 2f, 2L } }.Values };
137+
138+
yield return new object[] { new Dictionary<double, float> { { 1.0, 1.0f }, { 2.0, 2.0f } }.Keys };
139+
yield return new object[] { new Dictionary<float, double> { { 1.0f, 1.0 }, { 2.0f, 2.0 } }.Values };
140+
yield return new object[] { new SortedDictionary<Guid, string> { { Guid.NewGuid(), "One" }, { Guid.NewGuid(), "Two" } }.Keys };
141+
yield return new object[] { new SortedDictionary<long, Guid> { { 1L, Guid.NewGuid() }, { 2L, Guid.NewGuid() } }.Values };
142+
}
143+
144+
public static IEnumerable<object[]> TestDebuggerAttributes_InputsPresentedAsDictionary()
145+
{
146+
#if !NETFRAMEWORK
147+
return TestDebuggerAttributes_NonGenericDictionaries()
148+
.Concat(TestDebuggerAttributes_GenericDictionaries());
149+
#else
150+
// In .Net Framework only non-generic dictionaries are displayed in a dictionary format by the debugger.
151+
return TestDebuggerAttributes_NonGenericDictionaries();
152+
#endif
153+
}
154+
155+
public static IEnumerable<object[]> TestDebuggerAttributes_InputsPresentedAsList()
156+
{
157+
#if !NETFRAMEWORK
158+
return TestDebuggerAttributes_ListInputs();
159+
#else
160+
// In .Net Framework generic dictionaries are displayed in a list format by the debugger.
161+
return TestDebuggerAttributes_GenericDictionaries()
162+
.Select(t => new[] { t[0] })
163+
.Concat(TestDebuggerAttributes_ListInputs());
164+
#endif
165+
}
166+
167+
public static IEnumerable<object[]> TestDebuggerAttributes_Inputs()
168+
{
169+
return TestDebuggerAttributes_InputsPresentedAsDictionary()
170+
.Select(t => new[] { t[0] })
171+
.Concat(TestDebuggerAttributes_InputsPresentedAsList());
61172
}
62173

63174
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
64-
[MemberData(nameof(TestDebuggerAttributes_Inputs))]
65-
public static void TestDebuggerAttributes(object obj)
175+
[MemberData(nameof(TestDebuggerAttributes_InputsPresentedAsDictionary))]
176+
public static void TestDebuggerAttributes_Dictionary(IDictionary obj, KeyValuePair<string, string>[] expected)
177+
{
178+
DebuggerAttributes.ValidateDebuggerDisplayReferences(obj);
179+
DebuggerAttributeInfo info = DebuggerAttributes.ValidateDebuggerTypeProxyProperties(obj);
180+
PropertyInfo itemProperty = info.Properties.Single(pr => pr.GetCustomAttribute<DebuggerBrowsableAttribute>().State == DebuggerBrowsableState.RootHidden);
181+
Array itemArray = (Array)itemProperty.GetValue(info.Instance);
182+
List<KeyValuePair<string, string>> formatted = itemArray.Cast<object>()
183+
.Select(DebuggerAttributes.ValidateFullyDebuggerDisplayReferences)
184+
.Select(formattedResult => new KeyValuePair<string, string>(formattedResult.Key, formattedResult.Value))
185+
.ToList();
186+
187+
CollectionAsserts.EqualUnordered((ICollection)expected, formatted);
188+
}
189+
190+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
191+
[MemberData(nameof(TestDebuggerAttributes_InputsPresentedAsList))]
192+
public static void TestDebuggerAttributes_List(object obj)
66193
{
67194
DebuggerAttributes.ValidateDebuggerDisplayReferences(obj);
68195
DebuggerAttributeInfo info = DebuggerAttributes.ValidateDebuggerTypeProxyProperties(obj);

src/libraries/Common/tests/System/Diagnostics/DebuggerAttributes.cs

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5-
using System.Diagnostics;
5+
using System.Data;
66
using System.Linq;
77
using System.Reflection;
88
using System.Text;
@@ -15,6 +15,13 @@ internal class DebuggerAttributeInfo
1515
public IEnumerable<PropertyInfo> Properties { get; set; }
1616
}
1717

18+
internal class DebuggerDisplayResult
19+
{
20+
public string Value { get; set; }
21+
public string Key { get; set; }
22+
public string Type { get; set; }
23+
}
24+
1825
internal static class DebuggerAttributes
1926
{
2027
internal static object GetFieldValue(object obj, string fieldName)
@@ -86,18 +93,52 @@ public static IEnumerable<PropertyInfo> GetDebuggerVisibleProperties(Type debugg
8693

8794
public static Type GetProxyType(Type type) => GetProxyType(type, type.GenericTypeArguments);
8895

89-
private static Type GetProxyType(Type type, Type[] genericTypeArguments)
96+
internal static DebuggerDisplayResult ValidateFullyDebuggerDisplayReferences(object obj)
97+
{
98+
CustomAttributeData cad = FindAttribute(obj.GetType(), attributeType: typeof(DebuggerDisplayAttribute));
99+
100+
// Get the text of the DebuggerDisplayAttribute
101+
string attrText = (string)cad.ConstructorArguments[0].Value;
102+
string formattedValue = EvaluateDisplayString(attrText, obj);
103+
104+
string formattedKey = FormatDebuggerDisplayNamedArgument(nameof(DebuggerDisplayAttribute.Name), cad, obj);
105+
string formattedType = FormatDebuggerDisplayNamedArgument(nameof(DebuggerDisplayAttribute.Type), cad, obj);
106+
107+
return new DebuggerDisplayResult { Value = formattedValue, Key = formattedKey, Type = formattedType };
108+
}
109+
110+
internal static string ValidateDebuggerDisplayReferences(object obj)
111+
{
112+
CustomAttributeData cad = FindAttribute(obj.GetType(), attributeType: typeof(DebuggerDisplayAttribute));
113+
114+
// Get the text of the DebuggerDisplayAttribute
115+
string attrText = (string)cad.ConstructorArguments[0].Value;
116+
117+
return EvaluateDisplayString(attrText, obj);
118+
}
119+
120+
private static CustomAttributeData FindAttribute(Type type, Type attributeType)
90121
{
91-
// Get the DebuggerTypeProxyAttribute for obj
92-
CustomAttributeData[] attrs =
93-
type.GetTypeInfo().CustomAttributes
94-
.Where(a => a.AttributeType == typeof(DebuggerTypeProxyAttribute))
95-
.ToArray();
96-
if (attrs.Length != 1)
122+
for (Type t = type; t != null; t = t.BaseType)
97123
{
98-
throw new InvalidOperationException($"Expected one DebuggerTypeProxyAttribute on {type}.");
124+
CustomAttributeData[] attributes = t.GetTypeInfo().CustomAttributes
125+
.Where(a => a.AttributeType == attributeType)
126+
.ToArray();
127+
if (attributes.Length != 0)
128+
{
129+
if (attributes.Length > 1)
130+
{
131+
throw new InvalidOperationException($"Expected one {attributeType.Name} on {type} but found more.");
132+
}
133+
return attributes[0];
134+
}
99135
}
100-
CustomAttributeData cad = attrs[0];
136+
throw new InvalidOperationException($"Expected one {attributeType.Name} on {type}.");
137+
}
138+
139+
private static Type GetProxyType(Type type, Type[] genericTypeArguments)
140+
{
141+
CustomAttributeData cad = FindAttribute(type, attributeType: typeof(DebuggerTypeProxyAttribute));
101142

102143
Type proxyType = cad.ConstructorArguments[0].ArgumentType == typeof(Type) ?
103144
(Type)cad.ConstructorArguments[0].Value :
@@ -110,24 +151,24 @@ private static Type GetProxyType(Type type, Type[] genericTypeArguments)
110151
return proxyType;
111152
}
112153

113-
internal static string ValidateDebuggerDisplayReferences(object obj)
154+
private static string FormatDebuggerDisplayNamedArgument(string argumentName, CustomAttributeData debuggerDisplayAttributeData, object obj)
114155
{
115-
// Get the DebuggerDisplayAttribute for obj
116-
Type objType = obj.GetType();
117-
CustomAttributeData[] attrs =
118-
objType.GetTypeInfo().CustomAttributes
119-
.Where(a => a.AttributeType == typeof(DebuggerDisplayAttribute))
120-
.ToArray();
121-
if (attrs.Length != 1)
156+
CustomAttributeNamedArgument namedAttribute = debuggerDisplayAttributeData.NamedArguments.FirstOrDefault(na => na.MemberName == argumentName);
157+
if (namedAttribute != default)
122158
{
123-
throw new InvalidOperationException($"Expected one DebuggerDisplayAttribute on {objType}.");
159+
string? value = (string?)namedAttribute.TypedValue.Value;
160+
if (!string.IsNullOrEmpty(value))
161+
{
162+
return EvaluateDisplayString(value, obj);
163+
}
124164
}
125-
CustomAttributeData cad = attrs[0];
126-
127-
// Get the text of the DebuggerDisplayAttribute
128-
string attrText = (string)cad.ConstructorArguments[0].Value;
165+
return "";
166+
}
129167

130-
string[] segments = attrText.Split(new[] { '{', '}' });
168+
private static string EvaluateDisplayString(string displayString, object obj)
169+
{
170+
Type objType = obj.GetType();
171+
string[] segments = displayString.Split(['{', '}']);
131172

132173
if (segments.Length % 2 == 0)
133174
{

src/libraries/System.Collections.NonGeneric/src/System.Collections.NonGeneric.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
<Compile Include="System\Collections\SortedList.cs" />
1717
<Compile Include="System\Collections\Stack.cs" />
1818
<Compile Include="System\Collections\Specialized\CollectionsUtil.cs" />
19-
<Compile Include="$(CoreLibSharedDir)System\Collections\KeyValuePairs.cs"
20-
Link="Common\System\Collections\KeyValuePairs.cs" />
19+
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\DebugViewDictionaryItem.cs"
20+
Link="Common\System\Collections\Generic\DebugViewDictionaryItem.cs" />
2121
</ItemGroup>
2222

2323
<ItemGroup>

src/libraries/System.Collections.NonGeneric/src/System/Collections/SortedList.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
**
1212
===========================================================*/
1313

14+
using System.Collections.Generic;
1415
using System.Diagnostics;
1516
using System.Diagnostics.CodeAnalysis;
1617
using System.Globalization;
@@ -351,12 +352,12 @@ public virtual void CopyTo(Array array, int arrayIndex)
351352
// KeyValuePairs is different from Dictionary Entry in that it has special
352353
// debugger attributes on its fields.
353354

354-
internal virtual KeyValuePairs[] ToKeyValuePairsArray()
355+
internal virtual DebugViewDictionaryItem<object, object?>[] ToDebugViewDictionaryItemArray()
355356
{
356-
KeyValuePairs[] array = new KeyValuePairs[Count];
357+
var array = new DebugViewDictionaryItem<object, object?>[Count];
357358
for (int i = 0; i < Count; i++)
358359
{
359-
array[i] = new KeyValuePairs(keys[i], values[i]);
360+
array[i] = new DebugViewDictionaryItem<object, object?>(keys[i], values[i]);
360361
}
361362
return array;
362363
}
@@ -766,9 +767,9 @@ public override void SetByIndex(int index, object? value)
766767
}
767768
}
768769

769-
internal override KeyValuePairs[] ToKeyValuePairsArray()
770+
internal override DebugViewDictionaryItem<object, object?>[] ToDebugViewDictionaryItemArray()
770771
{
771-
return _list.ToKeyValuePairsArray();
772+
return _list.ToDebugViewDictionaryItemArray();
772773
}
773774

774775
public override void TrimToSize()
@@ -1097,11 +1098,11 @@ public SortedListDebugView(SortedList sortedList)
10971098
}
10981099

10991100
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
1100-
public KeyValuePairs[] Items
1101+
public DebugViewDictionaryItem<object, object?>[] Items
11011102
{
11021103
get
11031104
{
1104-
return _sortedList.ToKeyValuePairsArray();
1105+
return _sortedList.ToDebugViewDictionaryItemArray();
11051106
}
11061107
}
11071108
}

0 commit comments

Comments
 (0)