asp.net-mvcnhibernateorchardcms

Want to save selected (i.e., more than 1) enums as string with NHibernate


I cannot for the life of me get this to work with my existing code, but I am trying to save my enum selections as strings in NHibernate. Basically, I have a UI check box and if the user selects multiple check boxes I want to store those selections. Now, I can get NHibernate to store ONE selection (e.g., from a drop down or radio button list, where the user is limited to one choice only).

This is the jist of what I have for an enum:

public enum IncomeType
{
    [Display(Name = "Full-Time Employment")]
    FullTime,
    [Display(Name = "Part-Time Employment")]
    PartTime,
    [Display(Name = "Self-Employment")]
    SelfEmployed,
    [Display(Name = "Rental")]
    Rental,
    [Display(Name = "Social Security Payments")]
    SocialSecurity,
    [Display(Name = "Retirement / Pension Payments")]
    Retirement,
    [Display(Name = "Child Support Payments")]
    ChildSupport,
    [Display(Name = "Spousal Maintenance")]
    Maintenance,
    [Display(Name = "Other")]
    Other
}

I use a method to "select" whether a checkbox list is shown (if my BulkItemThreshold equals the number of options, a checkbox list is displayed). Here is that method:

public static IEnumerable<SelectListItem> GetItemsFromEnumString<T>
    (T selectedValue = default(T)) where T : struct
{
    return from name in Enum.GetNames(typeof(T))
       let enumValue = Convert.ToString((T)Enum.Parse(typeof(T), name, true))
    
    select new SelectListItem
    {
        Text = GetEnumDescription(name, typeof(T)),
        Value = enumValue,
        Selected = enumValue.Equals(selectedValue)
    };
}

(Note: some items in there are helpers, but I don't believe they are relevant; also, the selected input is displayed using a template .cshtml file - again, not sure if that's relevant)

Now, I call this thusly:

public class IncomeTypeSelectorAttribute : SelectorAttribute
{
    public override IEnumerable<SelectListItem> GetItems()
    {
        return Selector.GetItemsFromEnumString<IncomeType>();
    }
}

And finally, we get to the virtual property (using a proxy) but this is where NHibernate throws a wrench (Note: this was working fine for me before NHibernate, and now I am trying to get many lines of code working with it WITHOUT having to re-do everything; if I re-do everything I will probably triple the code I already have to get it to work):

Property (record):

[IncomeTypeSelector(BulkSelectionThreshold = 9)]
public virtual List<string> IndividualIncomeTypeCheckBox { get; set; }

proxy (part):

public List<string> IndividualIncomeTypeCheckBox
{
    get { return Record.IndividualIncomeTypeCheckBox; }
    set { Record.IndividualIncomeTypeCheckBox = value; }
}

Again, this is how I was doing things and it was working great before NHibernate. But now I have to use NHibernate. No getting around it.

I am using a service class that it tying the two together in a Create method to save in the DB with NHibernate, and for the above it would ordinarily look like this:

 part.IndividualIncomeTypeCheckBox = record.IndividualIncomeTypeCheckBox;

This would work if it were just one selection.

Well, I've spent a good two (2) months trying to get this to work. It's tough because I have lots of code where the user can make only one selection (such as with a radiobutton list) and it works GREAT - even with NHibernate. Let me give you an example:

public virtual IncomeType? IndividualIncomeTypeCheckBox { get; set; }

If I do the above, it will display a drop down list, and NHibernate will store the ONE allowable option selected by the user in the DB no problem. But more than one option with List<string> does not work.

Now, I have tried everything I could find here or elsewhere and nothing works. Yes, I know it should be IList<IncomeType> or some other variant. But if I use this then NHibernate requires that IncomeType be another table in the DB. This is too much code to write for such a simple thing I believe. We are not talking a many-to-many relation in the sense that this is not a User with Multiple addresses (wherein addresses would have street, city, state, zip, etc.).

I have tried different types of proxy get and set code, but nothing works. I have tried [Flags] and other things working with string only, but to no avail. Those last solutions would "work" but ONLY to save the first item selected out of multiple (i.e., in my scenario, if the user selected "FullTime" and "Rental" as Income Types, then only "FullTime" (string) would be saved or "1" ([Flags]/int), not both items selected.

I have a situation where I re-display the choices using a ReadOnly attribute like this:

[IncomeTypeSelector]
[ReadOnly(true)]
public List<string> IndividualIncomeTypeCheckBoxPost
{
    get { return IndividualIncomeTypeCheckBox; }
}

This would display on the UI, but I tried doing something like this with NHibernate and it wouldn't work.

Could anyone please show me, using the above, how I can go about getting NHibernate to store more than one enum in this checkbox list scenario?

UPDATE: More poking around here and on the web, and I came up with the following (which still does not work).

Property (record):

[IncomeTypeSelector(BulkSelectionThreshold = 9)]
public virtual IList<IncomeTypeRecord> IndividualIncomeTypeCheckBox
{ 
    get { return incomeType; } 
    set { incomeType= value; } 
}
private IList<IncomeTypeRecord> incomeType = 
    new List<IncomeTypeRecord>();

Proxy (part):

public IList<IncomeTypeRecord> IndividualIncomeTypeCheckBox
{
    get { return Record.IndividualIncomeTypeCheckBox; }
    set { Record.IndividualIncomeTypeCheckBox= value; }
}

And a change to the enum:

public enum IncomeType : int // removing int & value still gives validate error
{
[Display(Name = "Full-Time Employment")]
FullTime = 1,
[Display(Name = "Part-Time Employment")]
PartTime,
....
}

And I added this class to support IncomeTypeRecord

public class IncomeTypeRecord
{
    public virtual int Id { get; set; }
    public virtual IncomeType Value { get; set; }
}

HOWEVER, when I get to the UI screen and pick one or more options I get a validation error (value not valid). For example, say I pick FullTime alone, or pick FullTime and Retirement, then the UI will display the following error:

The value 'FullTime' is invalid.

The value 'FullTime,Retirement' is invalid.

(respectively)

Even if I remove the int declaration for the enum and get rid of the value I started with "1", I still get this validation error. I tried messing around with and adding different model binders (which now has me stumped as to whether my original problem still exists and now I have a different problem - but you still get bounty points :) ).

Pulling my hair out. If I could offer more bounty I would. I need a definitive solution. I appreciate any help.

UPDATE This is what I have so far:

Record:

public virtual string IndividualIncomeTypeCheckBox{ get; set; }

Part:

// If I do IEnumerable<string> my .Select throws a cast error
public IEnumerable<IncomeType> IndividualIncomeTypeCheckBox
{
        get
        {
            return Record
                .IndividualIncomeTypeCheckBox
                .Split(',')
                .Select(r => (IncomeType)Enum.Parse(typeof(IncomeType), r));
        }
        set { Record.IndividualIncomeTypeCheckBox= value 
            == null ? null : String.Join(",", value); }
}

Service class:

public SimplePart CreateSimple(SimplePartRecord record)
{
    SimplePart simple = Services.ContentManager.Create<SimplePart>("Simple");
    ...
    //How I would save a FirstName property (example Part / PartRecord below)
    //public virtual string FirstName { get; set; } - PartRecord
    //public string FirstName - Part
    //{
    //    get { return Record.FirstName ; }
    //    set { Record.FirstName= value; }
    //}
    simple.FirstName = record.FristName;
    ...
    //I obviously cannot do the following with the above IncomeType
    //Getting cannot convert string to IEnumerable error
    //How would I write this:
    simple.IndividualIncomeTypeCheckBox = record.IndividualIncomeTypeCheckBox;
    ...
}

And this is how it's called in a controller (this persists to DB): (Updating Controller code)

public ActionResult Confirm(string backButton, string nextButton)
{
    if (backButton != null)
        return RedirectToAction("WrapUp");
    else if ((nextButton != null) && ModelState.IsValid)
    {
        _myService.CreateSimple(myData.SimplePartRecord);
        return RedirectToAction("Submitted");
    }
    else
        return View(myData);
}

Updating with additional code (serialization and view model):

"myData" is defined in the controller (using Serialization) as:

private MyViewModel myData;
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    var serialized = Request.Form["myData"];
    if (serialized != null)
    {
        myData = (MyViewModel)new MvcSerializer().Deserialize
            (serialized, SerializationMode.Signed);
        TryUpdateModel(myData);
    }
    else
        myData = (MyViewModel)TempData["myData"] ?? new MyViewModel();
    TempData.Keep();
}
protected override void OnResultExecuted(ResultExecutedContext filterContext)
{
   if (filterContext.Result is RedirectToRouteResult)
        TempData["myData"] = myData;
}

I use Serialization because I set up a multi-step wizard (as seen in the controller action "backButton" "nextButton) on the front-end. I am not using a driver (which can only display Admin or on the front-end but then only on .cshtml files directly under the ~/Views folder (not in a structured folder list like I am using)). No driver = no update view model type code = no mechanism to "create" the data in the DB. If I do not use some "create" type method, the form will submit but all the data will be "NULL".

When you say that the data should be persisted automatically, I am sorry but I do not see how. All the stuff I read or code I review has SOME method of updating the DB with whatever is entered in a form. If I am missing something, my apologies.

"MyViewModel" is pretty straightforward:

[Serializabel]
public class MyViewModel
{
    public SimplePartRecord SimplePartRecord { get; set; }
}

And, just in case, here is the relevant portion of the migration (return 1 is a completely separate and unrelated table):

public int UpdateFrom1()
{
SchemaBuilder.CreateTable("SimplePartRecord",
    table => table
    .ContentPartRecord()
        ...
        .Column("IndividualIncomeTypeCheckBox", DbType.String)
        ...
    );
ContentDefinitionManager.AlterPartDefinition("SimplePart",
    part => part
    .Attachable(false));
return 2;
}

The error I am getting is

Cannot implicitly convert type 'string' to 'System.Collections.Generic.IEnumerable'"

when I do the following in the "Create" method of my service class:

simple.IndividualIncomeTypeCheckBox = record.IndividualIncomeTypeCheckBox;

One additional thought: I tried using the n-n Relation sample to handle this scenario. Aside from it being a lot of extra code for what I thought should be straightforward and simple, because of the way I am using Serialization I had a lot of object reference errors and could not figure out how to properly code my controller to handle it.


Solution

  • The problem is simply that it won't be able to map a List without building a full relationship with an intermediate association table. It is way simpler to have the record store the values as a comma-separated string (so your record property is a string, not a list of string) and your part can map back and forth between string and List.

    You can find an example of something very close here:

    https://bitbucket.org/bleroy/nwazet.commerce/src/d722cbebea525203b22c445905c9f28d2af7db46/Models/ProductAttributesPartRecord.cs?at=default

    https://bitbucket.org/bleroy/nwazet.commerce/src/d722cbebea525203b22c445905c9f28d2af7db46/Models/ProductAttributesPart.cs?at=default

    It's not using enum values, instead it's a list of ids, but that should give you a good idea about how to make this work fairly simply: parsing enums you already know how to do.

    Let me know if you need more details, but I think that's what you needed to get unblocked.