1

I am able to convert an enum to a string with the following code. However, it only stores ONE of the selected values. In the case where TWO values are selected, it is truncated when I store it using NHibernate.

Here is my code:

MyEnum { One, Two, Three, Four, Five }

private static readonly string[] myEnum =
    Enum.GetNames(typeof(MyEnum));
public string MyProperty
{
    get {
        var value = new MyEnum();
        int i = (int)value;
        return i >= 0 && i < myEnum.Length ?
            myEnum[i] : i.ToString(); }
    set { 
        Record.MyProperty= value == 
            null ? null : String.Join(",", value); }
}

Record is just public virtual string MyProperty { get; set; }

Can anyone provide a sample of how I would store, for example in comma-separated form, multiple enum's that are selected (e.g., "One, Two, Five" are selected by the user and all three are stored in the DB)?

UPDATE:

I am trying to do this in the get{}:

foreach (int i in Enum.GetValues(typeof(MyEnum)))
{
    return i >= 0 && i < myEnum.Length ? myEnum[i] : i.ToString();
}

but am getting a not all code paths return a value error.

QUESTION UPDATE:

If I did this with two string's:

part.MyProperty = record.MyProperty;

Using IEnumerable<MyEnum> answer below from @Jamie Ide won't work because I cannot convert string to MyEnum.

How would I write that last part to get the IEnumerable<MyEnum> code from @Jamie Ide in the answer below to work?

M E Moriarty
  • 231
  • 1
  • 6
  • 15
  • @frictionlesspulley Would you be able to roll up an example in an Answer that I can then check off if it works? Thanks! – M E Moriarty Dec 30 '12 at 18:23
  • The MyProperty you show accepts a single string in it's setter and does string.Join on that, which looks "unexpected". Everything you have takes and returns string - why are you not exposing the MyEnum more? And why myEnum? It contains the same values as someEnum.ToString() will give you. Why not store it as a ISet? – Oskar Berggren Dec 31 '12 at 10:21
  • The reason for the `not all code paths return a value` error is because nothing will be returned if there are no values in your Enum (i.e. the loop never runs). You could just add a dummy `return ""` at the end to make the compiler happy. – Eric Petroelje Dec 31 '12 at 14:38
  • Is it a requirement to store as a string? Have you considered storing as an `int` or `long` and adding the `[Flags]` attribute to the enum, then doing bitwise comparisons to get the list of enums? – Josh Anderson Jan 02 '13 at 11:50
  • @Josh I tried something along those lines but was unable to get it to work properly. – M E Moriarty Jan 02 '13 at 15:15

2 Answers2

3

As mentioned in my comment, unless it's an absolute requirement to store the enum as a string I'd recommend setting your enum up with the [Flags] attribute and creating a convention to store the enum as an int. Here's what I use (note I'm using FluentNH):

The convention

public class EnumConvention : IUserTypeConvention
{
    public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria)
    {
        // You can use this if you don't want to support nullable enums
        // criteria.Expect(x => x.Property.PropertyType.IsEnum);

        criteria.Expect(x => x.Property.PropertyType.IsEnum ||
            (x.Property.PropertyType.IsGenericType &&
             x.Property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) &&
             x.Property.PropertyType.GetGenericArguments()[0].IsEnum)
            );
    }

    public void Apply(IPropertyInstance target)
    {
        target.CustomType(target.Property.PropertyType);
    }
}

The enum
Note you have to be careful what you set the values to in order to keep the bitwise comparison working.

// Note I use long to support enough enum values
[Flags]
public enum MyEnum : long
{
    Foo = 1,
    Bar = 1 << 1,
    Baz = 1 << 2
}

You shouldn't have to do anything beyond that, I believe. No need to iterate to set or retrieve, nothing. To test for the presence of a value, you can use the HasFlag() extension method on the enum.

Update

To modify the MvcGrabBag code to support this kind of enum, you'll need to change the GetItemsFromEnum method as follows:

public static IEnumerable<SelectListItem> GetItemsFromEnum<T>(T enumeration = default(T)) where T : struct
{
    FieldInfo[] fields = enumeration.GetType().GetFields(BindingFlags.Public | BindingFlags.Static);

    return from field in fields
                let value = Enum.Parse(enumeration.GetType(), field.Name)
                let descriptionAttributes = field.GetCustomAttributes(typeof(DescriptionAttribute), true)
                select new SelectListItem
                {
                    Text = descriptionAttributes.Length > 0
                                ? ((DescriptionAttribute)descriptionAttributes[0]).Description
                                : field.Name,
                    Value = Convert.ToInt64(value).ToString(),
                    Selected = (Convert.ToInt64(enumeration) & Convert.ToInt64(value)) == Convert.ToInt64(value)
                };
}

Note that I kept it as a generic only because I don't know what other aspects of that toolkit depend on that signature. You can see that it's not necessary, though -- you can remove the <T> and have the signature like public static IEnumerable<SelectListItem> GetItemsFromEnum(Enum enumeration) and it'll work fine.

Note also that this code is using my convention of supporting names derived from the Description attribute. I use this to let my labels be more human-readable. The enum would look like this:

[Flags]
public enum MyEnum : long
{
    [Description("Super duper foo")]
    Foo = 1,
    [Description("Super duper bar")]
    Bar = 1 << 1,
    // With no description attribute it will use the ToString value
    Baz = 1 << 2
}
Josh Anderson
  • 5,975
  • 2
  • 35
  • 48
  • Thanks for this, but the problem I mentioned I was having was in my use of a custom "Selector" attribute on the property (i.e., `[MySelector]`). Because I use this to auto select check box, radio button, drop down, etc. when using `enum`'s, I am getting a `The Field ... must be a number` as an error on validating the page. I assume this is because the "Selector" I use puts the `enum` in a `SelectList`. The code is long and I don't wanna waste your time, but if you wanna see it lemme know. – M E Moriarty Jan 02 '13 at 20:06
  • 2
    Yeah, I'd love to see it. I have similar requirements to show selections in a checkbox list, so if I can figure out how to make your custom selector work with flags it'll save me a few foreach loops. – Josh Anderson Jan 02 '13 at 20:10
  • Well, that's good to hear. Rather than post here is a link to CodePlex where I got it from: [MvcGrabBag on CodePlex](http://mvcgrabbag.codeplex.com/). It's under the "/Selectors" folder (`Selector.cs`). There is also a template under the "/Shared/EditorTemplates" folder (`Selector.cshtml`) - this one contains the logic to display the appropriate input. Hopefully it's useful to you. – M E Moriarty Jan 02 '13 at 20:44
  • 1
    You're going to have to modify the `Selector` class so that the value assigned in the `GetItemsFromEnum` method isn't the string value of the enum, it's the int value. You'll have to modify that entire block so that the conversion to string happens in the loop. – Josh Anderson Jan 02 '13 at 21:27
  • I was thinking along the same lines when I first encountered this problem, except 1) I am not a programmer by trade, and 2) I am using 20 or so other `enum`'s which are not affected by this (it's only multi-select inputs like checkboxes and multi-lists). So figuring out how to switch between checkboxes and the other inputs is beyond me. Thanks for trying. – M E Moriarty Jan 02 '13 at 21:33
  • That last sentence in your last comment "You'll have to modify that entire block so that the conversion to string happens in the loop" - were you referring to the loop in the `.cshtml` Editor Template? Or did you mean to say the conversion to `int` happens in the loop? – M E Moriarty Jan 03 '13 at 13:08
  • 2
    I meant in the `Selector` class. There's a method called `GetItemsFromEnum` that uses a linq `select` to create the `SelectList`. You can see that it pulls the string value of the enum, sets the text using a call to `.Wordify()` (which I assume adds spaces to a camel case name), then sets the value to the string value of the enum. If you modified that so that you used the int value of the enum and *then* converted to a string for the text value, you'd end up with the correct setup that would let the modelbinder recognize a Flags enum. – Josh Anderson Jan 03 '13 at 13:43
  • woops, how did you define `enumeration`? – M E Moriarty Jan 03 '13 at 15:39
  • 2
    Updated again with the full method. You can probably try to make it not generic and see if it still works. – Josh Anderson Jan 03 '13 at 18:02
  • @Josh Two questions: 1) Do you place the `EnumConvention` code in a separate project and reference, or in the same project and reference somehow in the code? 2) I cannot get this to save more than one selection, e.g., if I select what is `1` and `2` as `[Flags]` (which should save as `3`, right?), I only get `1` in the database. Any thoughts? Thanks. – REMESQ Jan 10 '13 at 12:33
  • I don't think the convention is the issue. You're getting back a collection of individual values when you post the form, which means your controller is going to have to expect a collection and merge them by setting a single enum variable using something along the lines of `MyEnum selections = value1 | value2 | value3`. Not sure if you're using a ViewModel, but if so you might want to store values as `List` and merge them for persisting via NH. – Josh Anderson Jan 10 '13 at 14:22
1

Here's a quick and dirty solution. In this solution, _myEnumString would be mapped as a private field and stored in the database. As mentioned in the comments, it is possible to store the array as a collection using another table as shown in the answer to this question or in one table as in this question.

    public enum MyEnum { One, Two, Three, Four, Five }

public class MyClass
{
    private string _myEnumString;

    public IEnumerable<MyEnum> MyEnums
    {
        get 
        { 
            return Array.ConvertAll(_myEnumString.Split(','), s => (MyEnum)Enum.Parse(typeof(MyEnum), s));
        }
        set
        {
            _myEnumString = string.Join(",", value.Select(v => v.ToString()));
        }
    }

}

UPDATE

Assuming record.MyProperty and part.MyProperty are both delimited strings of MyEnum names (e.g. "One,Two,Three") then you can map MyProperty and create a read only property to return the collection of MyEnum.

public class MyClass
{
    public string MyProperty { get; set; }

    public IEnumerable<MyEnum> MyEnums
    {
        get 
        { 
            return Array.ConvertAll(MyProperty.Split(','), s => (MyEnum)Enum.Parse(typeof(MyEnum), s));
        }
    }

}
Community
  • 1
  • 1
Jamie Ide
  • 48,427
  • 16
  • 81
  • 117
  • Looks viable, but now when I try to save the record in the DB, I get an error with the existing code I use. Please see the "Question Update" in my post above. Thanks. – M E Moriarty Dec 31 '12 at 23:12
  • Sorry that update's not working. If I do `part.MyProperty = record.MyProperty;` to save in the DB, I am getting a cast error (`string` to `IEnumerable`). Doing your update does nothing to fix that. How would I write `part.MyProperty = [What goes here]` – M E Moriarty Jan 01 '13 at 19:05
  • So this isn't true? "Assuming record.MyProperty and part.MyProperty are both delimited strings of MyEnum names..." I don't understand exactly what you're trying to do. – Jamie Ide Jan 01 '13 at 19:33
  • First part of my question is what I am trying to do. If I make both of those strings I can save ONE of the enums, but when I try the `foreach` loop to try and store MORE than just ONE, I get the error I specified. When I tried the code you first suggested I got the cast error (`string` to `IEnumerable`). – M E Moriarty Jan 01 '13 at 23:35