10

Anyone know of any code that duplicates how the DebuggerDisplayAttribute parses and gathers the resultant string?

I would like to create a custom attribute that does nearly the sample thing. Similiar to "When a breakpoint is hit..." where you can use a variable within curly braces, as in "{variable}".

I already handle simple cases, such as "{Name}", but something like "{Foo.Name}" requires extra reflection code that I need help with.

Basically, I want to parse a string using the rules defined in the DebuggerDisplayAttribute documentation. Currently, I can parse and resolve "I am {GetName()}". I need help with something like "Foo's Name: {Foo.Name}"

AMissico
  • 21,470
  • 7
  • 78
  • 106
  • You are attempting to change the behavior of Visual Studio itself. So while you could easily create your own attribute, you would need to extend Visual Studio to recognize and report on it. Even then, I'm not sure if you can change the behavior to that degree. If it is possible, this would be the place to start: http://msdn.microsoft.com/en-us/library/bb161946.aspx – JDB Jun 11 '12 at 23:00
  • @Cyborgx37; No. I want to duplicate the functionality only. To parse the string and gather the values through reflection. – AMissico Jun 12 '12 at 00:17
  • Unfortunately I don't think there's any way to do this, see this related question: http://stackoverflow.com/questions/2793965/does-there-exists-a-method-to-render-an-object-using-debuggerdisplayattribute It would be nice to see this functionality exposed in the Framework. Maybe you could request it on connect - if so I'll vote for it! – Joe Jun 13 '12 at 15:34
  • @Joe, interesting. I wonder if I can create a DebuggerDisplayAttribute, with the string from my custom-attribute and just have the debugger-attribute do the work. Which raises the question, does the debugger-attribute work in Release builds. Something I need to try. – AMissico Jun 13 '12 at 15:54
  • @AMissico, I think you're out of luck. The code to parse the DebuggerDisplayAttribute is part of Visual Studio, and therefore inaccessible to your code. The DebuggerDisplayAttribute attribute exists in Release builds (if you look at MSDN it is not decorated with ConditionalAttribute, unlike, say, SuppressMessageAttribute). So Visual Studio will use it when debugging release builds. – Joe Jun 13 '12 at 16:12
  • Have you envisaged the use of `DataBinder.Eval` (http://msdn.microsoft.com/en-us/library/4hx47hfe.aspx) ? It's not exactly the same, but it does a lot of things. It also has a GetPropertyValue method wich supports the Foo.Name thing - implicitely used by Eval. One drawback is it needs a reference to System.Web. – Simon Mourier Jun 18 '12 at 05:52

2 Answers2

5

Hopefully this code all fits... I made a non-reflection version of what you are trying to do, using Microsoft Roslyn and its C# Scripting ability to run the "code" in the attribute value as C# code.

To use this code, make a new C# project, and use NuGet to add a reference to Roslyn.

First the classes I'm using to test, just so you can see the attributes I tried.

using System.Diagnostics;

namespace DebuggerDisplayStrings
{
    [DebuggerDisplay("The Value Is {StringProp}.")]
    public class SomeClass
    {
        public string StringProp { get; set; }
    }

    [DebuggerDisplay("The Value Is {Foo.StringProp}.")]
    public class SomeClass2
    {
        public SomeClass Foo { get; set; }
    }

    [DebuggerDisplay("The Value Is {Seven() - 6}.")]
    public class SomeClass3
    {
        public int Seven()
        {
            return 7;
        }
    }
}

Now the tests (yes these all pass):

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DebuggerDisplayStrings
{
    [TestClass]
    public class DebuggerDisplayReaderTests
    {
        [TestMethod]
        public void CanReadStringProperty()
        {
            var target = new SomeClass {StringProp = "Foo"};
            var reader = new DebuggerDisplayReader();
            Assert.AreEqual("The Value Is Foo.", reader.Read(target));
        }

        [TestMethod]
        public void CanReadPropertyOfProperty()
        {
            var target = new SomeClass2 {Foo = new SomeClass {StringProp = "Foo"}};
            var reader = new DebuggerDisplayReader();
            Assert.AreEqual("The Value Is Foo.", reader.Read(target));
        }

        [TestMethod]
        public void CanReadMethodResultAndDoMath()
        {
            var target = new SomeClass3();
            var reader = new DebuggerDisplayReader();
            Assert.AreEqual("The Value Is 1.", reader.Read(target));
        }
    }
}

Finally, the real goods:

using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using Roslyn.Scripting.CSharp;

namespace DebuggerDisplayStrings
{
    public class DebuggerDisplayReader
    {
        // Get the fully evaluated string representation of the DebuggerDisplayAttribute's value.
        public string Read(object target)
        {
            var debuggerDisplayFormat = GetDebuggerDisplayFormat(target);
            if(string.IsNullOrWhiteSpace(debuggerDisplayFormat))
                return target.ToString();
            return EvaluateDebuggerDisplayFormat(debuggerDisplayFormat, target);
        }

        // Gets the string off the attribute on the target class, or returns null if attribute not found.
        private static string GetDebuggerDisplayFormat(object target)
        {
            var attributes = target.GetType().GetCustomAttributes(typeof(DebuggerDisplayAttribute), false);
            return attributes.Length > 0 ? ((DebuggerDisplayAttribute)attributes[0]).Value : null;
        }

        // Executes each bracketed portion of the format string using Roslyn,
        // and puts the resulting value back into the final output string.
        private string EvaluateDebuggerDisplayFormat(string format, object target)
        {
            var scriptingEngine = new ScriptEngine(new[] { GetType().Assembly });
            var formatInfo = ExtractFormatInfoFromFormatString(format);
            var replacements = new List<object>(formatInfo.FormatReplacements.Length);
            foreach (var codePart in formatInfo.FormatReplacements)
            {
                var result = scriptingEngine.Execute(codePart, target);
                replacements.Add((result ?? "").ToString());
            }
            return string.Format(formatInfo.FormatString, replacements.ToArray());
        }

        // Parse the format string from the attribute into its bracketed parts.
        // Prepares the string for string.Format() replacement.
        private static DebuggerDisplayFormatInfo ExtractFormatInfoFromFormatString(string format)
        {
            var result = new DebuggerDisplayFormatInfo();
            var regex = new Regex(@"\{(.*)\}");
            var matches = regex.Matches(format);
            result.FormatReplacements = new string[matches.Count];
            for (var i = matches.Count - 1; i >= 0; i-- )
            {
                var match = matches[i];
                result.FormatReplacements[i] = match.Groups[1].Value;
                format = format.Remove(match.Index + 1, match.Length - 2).Insert(match.Index+1, i.ToString(CultureInfo.InvariantCulture));
            }
            result.FormatString = format;
            return result;
        }
    }

    internal class DebuggerDisplayFormatInfo
    {
        public string FormatString { get; set; }
        public string[] FormatReplacements { get; set; }
    }
}

Hopefully that helps you out. It was only about an hour and a half of work, so the unit testing isn't complete by any means, and I'm sure there are bugs in there somewhere, but it should be a solid start, if you are OK with the Roslyn approach.

CodingWithSpike
  • 42,906
  • 18
  • 101
  • 138
  • http://blogs.msdn.com/b/visualstudio/archive/2011/10/19/introducing-the-microsoft-roslyn-ctp.aspx – AMissico Jun 12 '12 at 03:43
  • If this was my personal project, I would use the "Roslyn" approach. How hard to use reflection? Would it allow for recursion, as in "{property.property.property|method}" – AMissico Jun 12 '12 at 03:51
  • In my opinion, the reflection approach is completely possible, but significantly more work. You would just have to do all the parsing yourself, which would probably come down to just separating the string my '.'s and recursively getting the value of the member. However the MSDN spec for that attribute says you can do math and other operators in the attribute, like `[DebuggerDisplay("{1 + 2 + 3}")]` would return the string "6", so parsing all that out yourself would be a lot of work, and that is where Roslyn comes in handy. However if you don't care to support that, then reflection would work. – CodingWithSpike Jun 12 '12 at 12:39
  • If you are locked-in to the reflection approach, for whatever reasons, then it might help to post whatever code you do have working so far. – CodingWithSpike Jun 12 '12 at 12:43
  • The code is in two projects and I didn't have time to combine them. What I did to get past this obstacle is to create a method that returns the resultant string and pass that method to the custom-attribute. Just like the answer of http://stackoverflow.com/questions/2793965/does-there-exists-a-method-to-render-an-object-using-debuggerdisplayattribute. – AMissico Jun 13 '12 at 16:00
  • In the LINQ examples provided in the Visual Studio samples, there is an `ObjectDumper`. I use that code whenever possible. It does not support recursion, but that is because the code itself limits recursion to one level, which can be changed. My thought was to combine what you did with the principals in `ObjectDumper`. – AMissico Jun 13 '12 at 16:04
2

I'm assuming this is for your own (team) use. I've not personally tried this, but have you looked at the explanations for how to customize DebuggerDisplay attribute found here?

  • I know how the attribute works. Like I mentioned, I can handle methods or properties on the main object. It is the properties and methods on the main object's properties that I need extra help with. – AMissico Jun 11 '12 at 22:42
  • I wasn't suggesting anything else, but I thought you might not have realized that you can actually (apparently) alter the way DebuggerDisplayAttribute expands by editing autoexp.cs. You're not explaining in your post exactly what functionality you are looking for. – 500 - Internal Server Error Jun 11 '12 at 22:46