4

I have the following set of enums:

[Flags]
public enum Categories : uint
{
    A = (1 << 0),
    B = (1 << 1),
    B1 = B | (1 << 16),
    B2 = B | (1 << 17),
    B3 = B | (1 << 18),
    B4 = B | (1 << 19),
    B5 = B | (1 << 20),
    C = (1 << 2),
    C1 = C | (1 << 21),
    D = (1 << 3),
    D1 = D | (1 << 22),
    D2 = D | (1 << 23),
    E = (1 << 4),
    F = (1 << 5),
    F1 = F | (1 << 23),
    F2 = F | (1 << 24),
    F3 = F | (1 << 25),
    F4 = F | (1 << 26),
    F5 = F | (1 << 27),
    G = (1 << 6),
    H = (1 << 7),
    H1 = H | (1 << 28),
}

The idea is that the enums represent a hierarchical structure where a child enum implies its parent and any number of flags can be applied.

The problem I am seeing is that all child enums are not being represented during debugging as names or sets of names. I.E., Categories.F = "F" but Categories.F2 = 16777248. I would have hoped Categories.F2 = "F, F2" or at least "F2"

How can I make my enums remain recognized as flags? Is there a better way to accomplish what I'm trying to do?

dlras2
  • 8,416
  • 7
  • 51
  • 90
  • I just compiled and ran your code, I got `Categories.F2.ToString() = "F2"` – p.s.w.g Apr 06 '13 at 06:39
  • @p.s.w.g Yes, sorry - it's actually only a problem in debugging watch windows, etc. I edited the question to clarify. – dlras2 Apr 06 '13 at 06:40
  • 2
    So the problem is only during debugging? I wonder if you add `[DebugerDisplay("{ToString()}")]` does that have any effect? – p.s.w.g Apr 06 '13 at 06:44
  • 2
    Problem can be reproduced if you have any flags enum where one composite enum constant consists of a named bit and an unnamed one. Example: `[Flags] enum Categories { F = 1, F2 = 1 | 2, }`. A value of `Categories` with underlying numeric value `3`, will be shown as `"3"` by the debugger, whereas `.ToString()` and `.ToString("F")` both give `"F2"`. It seems like a ***bug***. – Jeppe Stig Nielsen Apr 06 '13 at 07:47
  • @p.s.w.g Adding `[DebuggerDisplay("{ToString()}")]` attribute (while keeping `[Flags]` of course) works (`using System.Diagnostics` namespace). Then the debugger shows the same as `ToString()`. But is's strange that it is necessary. Why does the debugger have a different algorithm than `ToString()` by default? – Jeppe Stig Nielsen Apr 06 '13 at 10:19
  • @DanRasmussen thanks, I've included some detail about `DebuggerDisplay` and `DebuggerTypeProxy` in my answer for completeness. – p.s.w.g Apr 06 '13 at 13:31

2 Answers2

3

It's very strange that the value in the debugger is different from the ToString value. According to the documentation, the two should match (because the Enum type does indeed override ToString).

If a C# object has an overridden ToString(), the debugger will call the override and show its result instead of the standard {<typeName>}.

Obviously this is not working for enums. My best guess is that the debugger is trying to do some special, undocumented handling of enum types. Adding the DebuggerDisplayAttribute apparently resolves the issue by overriding this behavior.

[DebuggerDisplay("{ToString()}")]
[Flags]
public enum Categories : uint
{
    ...
}

Categories.F2.ToString() = "F, F2"

C# won't do that magic for you, because F2 already has it's own name in the enum. You can manually mark the the individual members like this:

public enum Categories
{
    [Description("F, F2")]
    F2 = F | (1 << 24),
}

And then write code to convert to the description.

public static string ToDescription(this Categories c)
{
    var field = typeof(Categories).GetField(c.ToString());
    if (field != null)
    {
        return field.GetCustomAttributes().Cast<DescriptionAttribute>().First().Description;
    }
}
...
Categories.F2.ToDescription() == "F, F2";

Or you could do a bit of magic to generate this yourself:

public static string ToDescription(this Categories c)
{
    var categoryNames =
        from v in Enum.GetValues(typeof(Categories)).Cast<Category>()
        where v & c == c
        orderby v
        select v.ToString();
    return String.Join(", ", categoryNames);
}

Unfortunately, an extension method cannot be used with DebuggerDisplayAttribute, but you can use DebuggerTypeAttribute, YMMV but you could try this:

[DebuggerType("CategoryDebugView")]
[Flags]
public enum Categories : uint
{
    ...
}

internal class CategoryDebugView
{
    private Category value;

    public CategoryDebugView(Category value)
    {
        this.value = value;
    }

    public override string ToString()
    {
        var categoryNames =
            from v in Enum.GetValues(typeof(Categories)).Cast<Category>()
            where v & c == c
            orderby v
            select v.ToString();
        return String.Join(", ", categoryNames);
    }
}
p.s.w.g
  • 146,324
  • 30
  • 291
  • 331
  • [DebuggerType] does not exist. [DebuggerTypeProxy] exists but cannot be applied to enums: "error CS0592: Attribute 'DebuggerTypeProxy' is not valid on this declaration type. It is only valid on 'assembly, class, struct' declarations." – robert4 Sep 14 '17 at 11:27
  • @robert4 my mistake, I was referring to `DebuggerTypeProxy`. I'll double-check this answer when I get to a computer. – p.s.w.g Sep 14 '17 at 12:35
1

You can do what you ask with a little work. I've created some extension methods on Categories which use HasFlag() to detemine whether the enum value has a particular parent and then calls ToString() on them and concatenates the result.

public static class CategoriesExtensionMethods
{
    public static Categories GetParentCategory(this Categories category)
    {
        Categories[] parents = 
        {
            Categories.A,
            Categories.B,
            Categories.C,
            Categories.D,
            Categories.E,
            Categories.F,
            Categories.G,
            Categories.H,
        };

        Categories? parent = parents.SingleOrDefault(e => category.HasFlag(e));
        if (parent != null)
            return (Categories)parent;
        return Categories.None;
    }

    public static string ToStringWithParent(this Categories category)
    {
        var parent = GetParentCategory(category);
        if (parent == Categories.None)
            return category.ToString();
        return string.Format("{0} | {1}", parent.ToString(), category.ToString());
    }
}

Then we can use it like this:

var f1 = Categories.F1;

var f1ParentString = f1.ToStringWithParent();
// f1ParentString = "F | F1"

var f = Categories.F;
var fParentString = f.GetParentCategory();
// fParentString = "F"

Update

Here is a more handsome way to go about implementing GetParentCategory() if you don't want to specify all of your parents.

public static Categories GetParentCategory(this Categories category)
{
    var values = Enum.GetValues(typeof(Categories)).Cast<Categories>();
    var parent = values.SingleOrDefault(e => category.HasFlag(e) && e != Categories.None && category != e);
    if (parent != Categories.None)
        return (Categories)parent;
    return Categories.None;
}
Daniel Imms
  • 47,944
  • 19
  • 150
  • 166