3

I have spent the better part of five (5) days trying to figure out how to make this work:

I use enum's to store "choices" for user input through either a radio button list, drop down list (these are one (1) choice out of several choices), as well as a check box list and multi-select list (these are one (1) to all out of several choices).

Now, with radio button lists and drop down lists I have no problem saving in the Orchard database. The user selects a choice, and that choice is saved as the particular enum chosen into the database.

However, with check box lists or multi-selelct lists, I cannot get Orchard/NHibernate to save the multiple selected enum's.

I have tried everything I could find here on SO or through a Google search. The only "viable" solution, which is overkill for this situation, is to create a new table (through migration)/part/part record combination just to store in some cases 7-8 choices. Then, of course, I can do something like public virtual IList<NewContentPartRecord> { get; set; }.

Yes, I have looked at Creating 1-N and N-N Relations in the Orchard docs. Someone thought that LazyField<T> might be a solution. But it appears (at least in info I have found or code samples I've looked throught) that LazyField<T> deals with a separate table scenario.

Please tell me I don't need a separate table for what I want to accomplish. Again, this seems like overkill.

Here is a simple example of what I am trying to do:

public enum MyEnum
{
    Enum1,
    Enum2,
    Enum3,
    Enum4,
    Enum5
}

The Selector.cs helps to automatically pick a radio button, drop down, check box or multi-select list:

public class MySelectorAttribute : SelectorAttribute
{
    public override IEnumerable<SelectListItem> GetItems()
    {
        return Selector.GetItemsFromEnum<MyEnum>();
    }
}

The PartRecord.cs:

[MyEnumSelector]
public virtual IList<string> MyEnumCheckBox { get; set; }

The Part.cs:

public IList<string> MyEnumCheckBox
{
    get { return Record.MyEnumCheckBox; }
    set { Record.MyEnumCheckBox = value; }
}

Note: when I use <string>, I get a "table doesn't exist" error. If I use <MyEnum> instead, I get a cast error (generic.list v. generic.icollection or some variation).

I have tried IEnumerable and ICollection all with varying error messages.

I have to imagine that Orchard/NHibernate would allow this type of behavior without requiring me to create a new table to reference to (which, again, seems like overkill in this scenario).

If anyone can help I'd greatly appreciate it. I'm at my wits end with this problem. Bounty? Cash? You name it. Yes, I am that desperate. :)

REMESQ
  • 1,190
  • 2
  • 26
  • 60

2 Answers2

4

You could decorate your enum MyEnum with [Flags] attribute and set the values of it's items to be a distinct powers of 2. For example, your enum might look like this:

[Flags]
public enum MyEnum
{
    Enum1 = 1,
    Enum2 = 2,
    Enum3 = 4,
    Enum4 = 8,
    Enum5 = 16
}

Now, your MyEnumCheckBox property of the PartRecord class would be of type int:

public virtual int MyEnumCheckBox { get; set; }

You could create a proxy property inside of the Part class. For example:

private IList<MyEnum> _myCheckBox;

[MyEnumSelector]
public IList<MyEnum> MyCheckBox
{
    get 
    {
        if (_myCheckBox == null)
        {
            _myCheckBox = new List<MyEnum>();

            foreach (MyEnum item in Enum.GetValues(typeof(MyEnum)))
            {
                if (((MyEnum)Record.MyEnumCheckBox & item) == item)
                    _myCheckBox.Add(item);
            }
        }

        return _myCheckBox;
    }
    set
    {
        _myCheckBox = value;
        Record.MyEnumCheckBox = 0;

        foreach (var item in value)
        {
           Record.MyEnumCheckBox |= (int)item;
        }
    }
}

You can find some more info on Flags attribute here. It basically serves to enable you to use multiple enum values for a single enum field what is exactly what you're looking for.


EDIT:

I've taken my time and built a custom module to demonstrate this technique. I've tested it and it works the way it should. So here's the source:

Migrations.cs

public int Create() 
{
  SchemaBuilder.CreateTable("MultipleEnumPickerRecord", table => table
    .ContentPartRecord()
    .Column<int>("SelectedItems"));

  ContentDefinitionManager.AlterPartDefinition("MultipleEnumPickerPart", p => p.Attachable());

  return 1;
}

Models:

[Flags]
public enum MyEnum
{
    Enum1 = 1, // bit-wise 00001 or 2^0
    Enum2 = 2, // bit-wise 00010 or 2^1
    Enum3 = 4, // bit-wise 00100 or 2^2
    Enum4 = 8, // bit-wise 01000 or 2^3
    Enum5 = 16 // bit-wise 10000 or 2^4
}

public class MultipleEnumPickerRecord : ContentPartRecord
{
    public virtual int SelectedItems { get; set; }
}

public class MultipleEnumPickerPart : ContentPart<MultipleEnumPickerRecord> 
{
    private IList<MyEnum> _selectedItems;

    public IList<MyEnum> SelectedItems
    {
        get
        {
            if (_selectedItems == null)
            {
                _selectedItems = new List<MyEnum>();

                foreach (MyEnum item in Enum.GetValues(typeof(MyEnum)))
                {
                    if (((MyEnum)Record.SelectedItems & item) == item)
                        _selectedItems.Add(item);
                }
            }

            return _selectedItems;
        }
        set
        {
            _selectedItems = value;
            Record.SelectedItems = 0;

            foreach (var item in value)
            {
                Record.SelectedItems |= (int)item;
            }
        }
    }
}

Handler:

public class MultipleEnumPickerHandler : ContentHandler
{
    public MultipleEnumPickerHandler(IRepository<MultipleEnumPickerRecord> repository)
    {
        Filters.Add(StorageFilter.For(repository));
    }
}

Driver:

public class MultipleEnumPickerDriver : ContentPartDriver<MultipleEnumPickerPart>
{

    protected override string Prefix { get { return "MultipleEnumPicker"; } }

    protected override DriverResult Editor(MultipleEnumPickerPart part, dynamic shapeHelper)
    {
        return ContentShape("Parts_MultipleEnumPicker_Edit", () => shapeHelper.EditorTemplate(
            TemplateName: "Parts/MultipleEnumPicker", Model: part, Prefix: Prefix));
    }

    protected override DriverResult Editor(MultipleEnumPickerPart part, IUpdateModel updater, 
        dynamic shapeHelper)
    {
        updater.TryUpdateModel(part, Prefix, null, null);
        return Editor(part, shapeHelper);
    }

}

Placement:

<Placement>
    <Place Parts_MultipleEnumPicker_Edit="Content:5"/>
</Placement>

And finally, the view:

@using ModuleNamespace.Models
@model MultipleEnumPickerPart
<fieldset>
  <div class="editor-label">@Html.LabelFor(x => x.SelectedItems)</div>
  <div class="editor-field">
    <select multiple="multiple" id="@Html.FieldIdFor(x => x.SelectedItems)" name="@Html.FieldNameFor(x => x.SelectedItems)">
      @foreach (MyEnum item in Enum.GetValues(typeof(MyEnum))) {
        var selected = Model.SelectedItems.Contains(item);
        <option value="@((int)item)" @if(selected) {<text>selected="selected"</text>}>
          @T(item.ToString())
        </option>
      }
    </select>
  </div>
</fieldset>

Hovewer, there are 2 things you have to keep in mind when you're implementing this techinque:

  1. Enums are treated internally as integers which means their values take up 32 bits. This in turn means that MyEnum can't have more than 32 enumeration items defined for this to work
  2. As Bertrand pointed out, database search for items will be harder (although, not impossible since major databases allow you to use bit-wise operators)

These both constraint could be bypassed by using different mapping function between your database and the model.

What does that mean?

In the example I've shown you, the database value (and the MultipleEnumPickerRecord value) is of type int while in the MultipleEnumPickerPart I've "mapped" that integer to a List<MyEnum>. This uses less space in the database and is faster than using some other mapping functions.

For example, you might use the string type for your database and MultipleEnumPickerRecord and then make some kind of a mapping to List<MyEnum> inside of MultipleEnumPickerPart. Most popular string mapping functions are

  • comma-delimited mapping - for example, if someone selected Enum1 and Enum4, you could map it to a string "Enum1,Enum4"
  • semicolon-delimited mapping - you'd map previous example to "Enum1;Enum4"

The type of delimiter to choose should be based on the character you know your string won't use. To deconstruct the list from a string in the database, you could use a simple value.Split(',').ToList(); (if you're using ',' as a delimiter).

This way, you're not bounded by only 32 enumeration items, and, since the value is saved as string, it's pretty straightforward to search in the database for some value. The downsides are that string will take a lot more space in the database (int will take space of one character from a string), and the string manipulation functions are somewhat slower than bit-wise function demonstrated in the sample above.

Community
  • 1
  • 1
Ivan Ferić
  • 4,725
  • 11
  • 37
  • 47
  • Thanks for this. Testing now. Hmmm...what am I missing to get `value.Items` working? Could not find definition `Generic.IList`. – REMESQ Dec 14 '12 at 17:39
  • Yes, it must have been typo. I've updated the answer. The difference is that instead of `value.Items` there should have been just `value`.. – Ivan Ferić Dec 14 '12 at 17:52
  • Ok! One more thing if I can impose: on `MyService.cs` I have `public Part CreatePart(PartRecord record) { Part part = Services.ContentManager.Create("Part"); part.MyEnumCheckBox = record.MyEnumCheckBox; }` and am getting an error (cannot implicity convert `int` to `System.Collections.Generic.IList`. How do I get around that? I coded my `Migration.cs` to change from `string` to `int16`. – REMESQ Dec 14 '12 at 17:59
  • 1
    `Part` doesn't have to expose all the properties that the `Record` has and so you don't need to use `part.MyEnumCheckBox = record.MyEnumCheckBox`. If you've defined `MyEnumCheckBox` property inside the `Part` class like I've shown in the answer, the `Part` will automatically pull the information from the `Record` and it will be available via `Part`'s `MyCheckBox` property. So, the answer is, don't call the `Record`'s `MyEnumCheckBox` directly. You should get it's value only via `Part`'s `MyCheckBox` property.. – Ivan Ferić Dec 14 '12 at 18:06
  • Sorry to be dense, but how do i do that? That is if I shouldn't do `part.MyEnumCheckBox = record.MyEnumCheckBox`, what should I write? – REMESQ Dec 14 '12 at 23:34
  • BTW, [this](http://stackoverflow.com/q/13744115/624479) is a question I placed a bounty on similar to this. So if you can help me figure this out then you can collect there as well. – REMESQ Dec 14 '12 at 23:56
  • I've updated my answer with a simple Orchard module using this technique. I tested it, and it works as it should. I'll answer your bounty question shortly :) – Ivan Ferić Dec 15 '12 at 13:11
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/21180/discussion-between-remesq-and-ivan-feric) – REMESQ Dec 15 '12 at 13:36
  • I got sidetracked with work and was happy to succeed on a Build of your answer. When I implemented the view (for which I assumed I had to do a `Html.RenderAction` because it's my viewmodel and the part model), I am getting an `object reference` error on this line: `var selected = Model.SelectedItems.Contains(item);`. Any thoughts? Thanks again. – REMESQ Dec 18 '12 at 13:53
  • Have you set the `SelectedItems` property? And more importantly, is your model's property that contains the list of selected enums called `SelectedItems` or did you name it differently? If you named it differently, you should change `SelectedItems` to that name.. – Ivan Ferić Dec 18 '12 at 14:33
  • Again, sorry for being dense, but where (and how) do I set that property. All the names correspond. I tried in the view and in the controller (my controller is using `Serializable` from MvcFutures and it's serialized with the View Model, and in the view model I did both `public Record Record { get; set; }` and `public Part Part { get; set;}` – REMESQ Dec 19 '12 at 12:31
  • I presume that the model for your view is the ViewModel you described right now. In the sample I've shown above, I used Part for the model of the view. So you need to do some adjustments by changing `var selected = Model.SelectedItems.Contains(item);` to `var selected = Model.Part.SelectedItems.Contains(item);`. And this is presuming that the name of your `IList` property inside the `Part` class is `SelectedItems`. If it is, on the other hand, named `ContactClientDayCheckBox` (based on our chat) you should change to `var selected = Model.Part.ContactClientDayCheckBox.Contains(item);` – Ivan Ferić Dec 19 '12 at 13:17
2

You simply cannot map a List<T> like that. You need to either create a proper relationship with another record/table, or you can manage the storage with, for example, a comma-separated list of values. The first case is of course a little cleaner, but the second one is easier and frankly quite alright if you don't expect too many value for a given record.

You might also want to consider an enum field.

Bertrand Le Roy
  • 17,731
  • 2
  • 27
  • 33
  • Thank you for the suggestions. Not very familiar with the CSV aspect, but I will dig a bit further. – REMESQ Dec 14 '12 at 17:21
  • 2
    Some more details then if you want to explore this. The way I do it is that the property on the record class is a string, so as far as nHib is concerned this really is a string. On the part, there are properties that do that mapping back and forth between the enum value list and the string representation. The recommendation to use flags is fine too, at least on the part API. Of course querying on individual values of that enumeration won't be possible without a lot of effort, but that's the price you pay for simpler storage. – Bertrand Le Roy Dec 14 '12 at 17:41
  • Sorry to bring this up, but the solution provided requires a lot of code re-write on my part to get it properly working. Was wondering if you knew of sample code that does what you describe in your last comment (string in record class, mapping in part between enum value list and string representation). Thanks in advance. – REMESQ Dec 27 '12 at 15:10
  • 2
    Well, an enum field requires not a single line of code. You just add it in the content type editor. As for storing a comma-separated list of values, look at the roles property on this: https://bitbucket.org/bleroy/nwazet.commerce/src/68d66c5a86e533244ec3eeb9bb08b627f71738e8/Models/DiscountPart.cs?at=default and the associated record: https://bitbucket.org/bleroy/nwazet.commerce/src/68d66c5a86e533244ec3eeb9bb08b627f71738e8/Models/DiscountPartRecord.cs?at=default It really isn't a whole lot of code. How about giving some recognition to my answer now? – Bertrand Le Roy Dec 27 '12 at 19:52
  • I see that it's tapping `UserRoles` from `Orchard.Roles`. In the `Discount.cs` class (the service?), I see that it's doing a check to see if the Roles are selected, but for the life of me I can't see how it's tied into actually "saving" the selected Roles in the DB. I am opening up another question which I'll certainly recognize as an answer: http://stackoverflow.com/q/14082926/624479 – REMESQ Dec 29 '12 at 15:06
  • What do you mean? The part's roles property does the back and forth translation from list to comma-separated string on the corresponding record property, which gets persisted to the database automatically. This is how it gets saved to the database, as a string. – Bertrand Le Roy Dec 29 '12 at 22:23