dlras2
dlras2

Reputation: 8496

How should I represent hierarchical flag enums?

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?

Upvotes: 4

Views: 731

Answers (2)

p.s.w.g
p.s.w.g

Reputation: 149040

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);
    }
}

Upvotes: 3

Daniel Imms
Daniel Imms

Reputation: 50229

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;
}

Upvotes: 1

Related Questions