Skip to content

Commit 0e09040

Browse files
committed
For for issue #75, variables display isn't displaying anything for generic dictionaries like PSBoundParametersDictionary. Also enhanced the determination of what is expandable. We were not allowing structs like DateTime, TimeSpan and DictionaryEntry to expand. Changed test from !IsValueType to !IsPrimitive. Also enhanced the display of property values where we encounter an exception when trying to retrieve the property. Following VS debugger's lead on this on. You can see the display by looking at $Host.Runspace. And we also weren't displaying .NET properties for colletions and dictionaries. We now do that - in addition to the elements in the collection or dictionary. Also, don't try to add properties that are indexers.
1 parent 6a83d00 commit 0e09040

File tree

2 files changed

+115
-47
lines changed

2 files changed

+115
-47
lines changed

src/PowerShellEditorServices/Debugging/VariableDetails.cs

Lines changed: 113 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ private static bool GetIsExpandable(object valueObject)
129129

130130
return
131131
valueObject != null &&
132-
!valueType.IsValueType &&
132+
!valueType.IsPrimitive &&
133+
!(valueObject is UnableToRetrievePropertyMessage) &&
133134
!(valueObject is string); // Strings get treated as IEnumerables
134135
}
135136

@@ -143,13 +144,23 @@ private static string GetValueString(object value, bool isExpandable)
143144
}
144145
else if (isExpandable)
145146
{
146-
Type objType = value.GetType();
147+
Type objType = value.GetType();
147148

148-
// Get the "value" for an expandable object. This will either
149-
// be the short type name or the ToString() response if ToString()
150-
// responds with something other than the type name.
151-
if (value.ToString().Equals(objType.FullName))
149+
// Get the "value" for an expandable object.
150+
if (value is DictionaryEntry)
152151
{
152+
// For DictionaryEntry - display the key/value as the value.
153+
var entry = (DictionaryEntry)value;
154+
valueString =
155+
string.Format(
156+
"[{0}, {1}]",
157+
entry.Key,
158+
GetValueString(entry.Value, GetIsExpandable(entry.Value)));
159+
}
160+
else if (value.ToString().Equals(objType.ToString()))
161+
{
162+
// If the ToString() matches the type name, then display the type
163+
// name in PowerShell format.
153164
string shortTypeName = objType.Name;
154165

155166
// For arrays and ICollection, display the number of contained items.
@@ -176,7 +187,8 @@ private static string GetValueString(object value, bool isExpandable)
176187
}
177188
else
178189
{
179-
if (value.GetType() == typeof(string))
190+
// ToString() output is not the typename, so display that as this object's value
191+
if (value is string)
180192
{
181193
valueString = "\"" + value + "\"";
182194
}
@@ -215,6 +227,11 @@ private static VariableDetails[] GetChildren(object obj)
215227
{
216228
List<VariableDetails> childVariables = new List<VariableDetails>();
217229

230+
if (obj == null)
231+
{
232+
return childVariables.ToArray();
233+
}
234+
218235
PSObject psObject = obj as PSObject;
219236
IDictionary dictionary = obj as IDictionary;
220237
IEnumerable enumerable = obj as IEnumerable;
@@ -223,59 +240,55 @@ private static VariableDetails[] GetChildren(object obj)
223240
{
224241
if (psObject != null)
225242
{
243+
// PowerShell wrapped objects can have extra ETS properties so let's use
244+
// PowerShell's infrastructure to get those properties.
226245
childVariables.AddRange(
227246
psObject
228247
.Properties
229248
.Select(p => new VariableDetails(p)));
230249
}
231-
else if (dictionary != null)
250+
else
232251
{
233-
childVariables.AddRange(
234-
dictionary
235-
.OfType<DictionaryEntry>()
236-
.Select(e => new VariableDetails(e.Key.ToString(), e.Value)));
237-
}
238-
else if (enumerable != null && !(obj is string))
239-
{
240-
int i = 0;
241-
foreach (var item in enumerable)
242-
{
243-
childVariables.Add(
244-
new VariableDetails(
245-
string.Format("[{0}]", i),
246-
item));
247-
248-
i++;
249-
}
250-
}
251-
else if (obj != null)
252-
{
253-
// Object must be a normal .NET type, pull all of its
254-
// properties and their values
255-
Type objectType = obj.GetType();
256-
var properties =
257-
objectType.GetProperties(
258-
BindingFlags.Public | BindingFlags.Instance);
259-
260-
foreach (var property in properties)
261-
{
262-
try
252+
// We're in the realm of regular, unwrapped .NET objects
253+
if (dictionary != null)
254+
{
255+
// Buckle up kids, this is a bit weird. We could not use the LINQ
256+
// operator OfType<DictionaryEntry>. Even though R# will squiggle the
257+
// "foreach" keyword below and offer to convert to a LINQ-expression - DON'T DO IT!
258+
// The reason is that LINQ extension methods work with objects of type
259+
// IEnumerable. Objects of type Dictionary<,>, respond to iteration via
260+
// IEnumerable by returning KeyValuePair<,> objects. Unfortunately non-generic
261+
// dictionaries like HashTable return DictionaryEntry objects.
262+
// It turns out that iteration via C#'s foreach loop, operates on the variable's
263+
// type which in this case is IDictionary. IDictionary was designed to always
264+
// return DictionaryEntry objects upon iteration and the Dictionary<,> implementation
265+
// honors that when the object is reintepreted as an IDictionary object.
266+
// FYI, a test case for this is to open $PSBoundParameters when debugging a
267+
// function that defines parameters and has been passed parameters.
268+
// If you open the $PSBoundParameters variable node in this scenario and see nothing,
269+
// this code is broken.
270+
int i = 0;
271+
foreach (DictionaryEntry entry in dictionary)
263272
{
264273
childVariables.Add(
265274
new VariableDetails(
266-
property.Name,
267-
property.GetValue(obj)));
275+
"[" + i++ + "]",
276+
entry));
268277
}
269-
catch (Exception)
278+
}
279+
else if (enumerable != null && !(obj is string))
280+
{
281+
int i = 0;
282+
foreach (var item in enumerable)
270283
{
271-
// Some properties can throw exceptions, add the property
272-
// name and empty string
273284
childVariables.Add(
274285
new VariableDetails(
275-
property.Name,
276-
string.Empty));
286+
"[" + i++ + "]",
287+
item));
277288
}
278289
}
290+
291+
AddDotNetProperties(obj, childVariables);
279292
}
280293
}
281294
catch (GetValueInvocationException)
@@ -290,6 +303,61 @@ private static VariableDetails[] GetChildren(object obj)
290303
return childVariables.ToArray();
291304
}
292305

306+
private static void AddDotNetProperties(object obj, List<VariableDetails> childVariables)
307+
{
308+
Type objectType = obj.GetType();
309+
var properties =
310+
objectType.GetProperties(
311+
BindingFlags.Public | BindingFlags.Instance);
312+
313+
foreach (var property in properties)
314+
{
315+
// Don't display indexer properties, it causes an exception anyway.
316+
if (property.GetIndexParameters().Length > 0)
317+
{
318+
continue;
319+
}
320+
321+
try
322+
{
323+
childVariables.Add(
324+
new VariableDetails(
325+
property.Name,
326+
property.GetValue(obj)));
327+
}
328+
catch (Exception ex)
329+
{
330+
// Some properties can throw exceptions, add the property
331+
// name and info about the error.
332+
if (ex.GetType() == typeof (TargetInvocationException))
333+
{
334+
ex = ex.InnerException;
335+
}
336+
337+
childVariables.Add(
338+
new VariableDetails(
339+
property.Name,
340+
new UnableToRetrievePropertyMessage(
341+
"Error retrieving property - " + ex.GetType().Name)));
342+
}
343+
}
344+
}
345+
293346
#endregion
347+
348+
private struct UnableToRetrievePropertyMessage
349+
{
350+
public UnableToRetrievePropertyMessage(string message)
351+
{
352+
this.Message = message;
353+
}
354+
355+
public string Message { get; }
356+
357+
public override string ToString()
358+
{
359+
return "<" + Message + ">";
360+
}
361+
}
294362
}
295363
}

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,14 @@ await this.debugService.SetBreakpoints(
201201
Assert.True(objVar.IsExpandable);
202202

203203
var objChildren = debugService.GetVariables(objVar.Id);
204-
Assert.Equal(2, objChildren.Length);
204+
Assert.Equal(9, objChildren.Length);
205205

206206
var arrVar = variables.FirstOrDefault(v => v.Name == "$arrVar");
207207
Assert.NotNull(arrVar);
208208
Assert.True(arrVar.IsExpandable);
209209

210210
var arrChildren = debugService.GetVariables(arrVar.Id);
211-
Assert.Equal(4, arrChildren.Length);
211+
Assert.Equal(11, arrChildren.Length);
212212

213213
var classVar = variables.FirstOrDefault(v => v.Name == "$classVar");
214214
Assert.NotNull(classVar);

0 commit comments

Comments
 (0)