简体   繁体   中英

Nested binding in ASP.NET MVC Razor

Description

A nested object needs to be bound to a dropdown, there already is a preselected value for the nested objects. The possible values are of an enum type. The dropdownlist with some other data will be posted back to the controller.

Code - types & classes:

[Serializable]
public enum DummyEnum
{
   DummyZero = 0,
   DummyOne = 1
}

public class Dummy
{
   public Guid Id { get; set; }
   public Dictionary<Guid, DummyEnum> DummyEnum { get; set; }
}

public class DummyViewModel
{
    public Dictionary<Guid, List<Dummy>> Dummies { get; set; }
}

public class DummyController
{
    private void Init(DummyViewModel model)
    {
       model.EnumList = Enum.GetValues(typeof(DummyEnum))
                    .Cast<DummyEnum>()
                    .Select(e => new SelectListItem
                    {
                        Value = (e).ToString(),
                        Text = e.ToString()
                    });
    }
}

HTML :

<td>
    @Html.DropDownListFor(
         m => m.Dummies[dummiesKey][dummyIndex]
                 .Enum[Id],
         new SelectList(Model.EnumList, "Value", "Text", e.ToString()))
</td>

<select 
    data-val="true" 
    data-val-required="The Enum field is required." 
    id="Dummies_guid__0__Enum_guid_" 
    name="Dummies[guid][0].Enum[guid]" 
    style="display: none;"
>
    <option value="DummyOne">DummyOne</option>
    <option selected="selected" value="DummyZero ">DummyZero</option>
</select>

Problem

The problem is that the model doesn't seem to be able to map the payload back to an object or misses the reference to the bound object. Everything is filled in correctly the guid, index and the value of the enum.

Payload:

Dummies[guid][0].Enum[guid]: DummyZero     
Dummies[guid][0].Enum[guid]: DummyZero 

Attempts

I tried with the following ideas but they weren't successfull for me.

What am I missing?

The front-end response will be in that form that you'll set it there. Then the ASP middleware will parse all those strings back to an object at your back-end.

So key moments here are:

  • a controller action parameter type - it could be any of your types but it should correlate with your front-end;
  • front-end's select element name attribute value - it should contain full existing path.

As I got from your code example the following.

  1. You have DummyViewModel view model class. It has property Dummies .
  2. You have Dummy class, that nested in DummyViewModel as Dummies . 2nd level dictionary.
  3. You have DummyEnum enum class, that is in use at DummyEnum values. Same names, different adjacent levels.
  4. The SelectList values are OK. They are directly from the enum.
  5. Based on the structure, to set up a first enum value you need to navigate its level by setting KEY and VALUE. Then do it again for another level. For me, the first enum value in this structure should have something like this:
Dummies[dummiesKeyGuid][dummyIndexId].DummyEnum[dummyEnumKeyGuid];

Where you have the following types in each step:

  • Dummies[dummiesKeyGuid] is <List<Dummy>> ;
  • Dummies[dummiesKeyGuid][dummyIndexId] is <Dummy> ;
  • Dummies[dummiesKeyGuid][dummyIndexId].DummyEnum[dummyEnumKeyGuid] is <DummyEnum> .

So @Html.DropDownListFor(...) should be updated to set the path as name .

Also:

  1. your action should take the Dummies type as a parameter.
ActionResult SomeFromProcessingAction(DummyViewModel Dummies)
  1. you should handle the passed stringified parameters map to the Dictionary type. It could be used outside of ASP (front-end) but has the issue. Please, check this post and its topic: https://stackoverflow.com/a/29820891/6344916 . Sometimes, it is easier to not use the Dictionary there. Just other classes like List or Array.

From your HTML example, I didn't get the m.Dummies type to have in its structure the Enum field. dummiesKey can't have "guid" value. GUID is another type that can be made ToString() easily but not otherwise.

IMHO. Too many Dummy s in the names. It confuses and breaks its understanding.

Also, its nested structure is VERY cumbersome. You could use smaller user forms to set the values and take smaller objects or event values on your back-end instead of the huge object parameters.

The List class has no mapping requirements, just denormalize your dictionaries and it will be easier to map them. The same with their navigation on the front-end. If required, you can make the List ToDictionary() . :)=)

For example, Dummy could be written using List<T> :

public class SomeElement
{
  public Guid Id { get; set; }
  public DummyEnum Enum { get; set; }
}

public class Dummy //kind of aggregation
{
  public Guid Id { get; set; }
  public List <SomeElement> DummyEnum { get; set; }
}

And so on with DummyViewModel . Or get rid of it and use some List directly.

Hope, this will help.

I think your issue is not related to nested properties or input naming and your implementation seems fine.

The issue you encountered is linked to Dictionary<Guid, ...> default model binder behavior. The default binder simply does not seem to handle it correctly. (ie. Dictionaries with Guid as keys)

I have reproduced your issue and then switched to Dictionary<string, ...> and everything worked fine this time.

The only way your could overcome this should probably be to implement your own model binder for Dictionary<Guid, object> .

I tried to understand the root problem and it seems to be located here (Invalid explicit cast from string to Guid) as also described here (found later:-)...)

The problem as stated in the question had to do with mvc converting Dictionary to a List<KeyValuePair<Guid, List>>binding or use JSON .

All that needs to be done is break down the object as mvc would and provide the necessary data. As explained in dicitionary binding .

The object was of type Dictionary<Guid, List<Dummy>> . So the object actually becomes List<KeyValuePair<Guid, List<List<KeyValuePair<Guid, enum>>>>> .

Now to break it down

MVC needs the index of the first object that is being used. To get this index we need to covert the dictionary to a list ourself. More specific the values or keys of the dictionarys.

var dummies= Model.Dummies[key];
var dummiesIndex = Model.Dummies.Values.ToList().IndexOf(dummies);

The index needs to be provided along side the post. This can be done by adding it above the dropdown as a hidden field along side the key from the dictionary.

@Html.Hidden("dummies.Index", dummiesIndex)
@Html.Hidden("dummies[" + dummiesIndex + "].Key", key)

Next is the List of objects. Again the index needs to be provided for the binding.

 @Html.Hidden("dummies[" + dummiesIndex + "].Value.Index", dummyIndex)

The last step is another dictionary, this is just like the first dictionary

@Html.Hidden("dummies[" + dummiesIndex + "].DummyEnum.Index", dummyEnumIndex)
@Html.Hidden("dummies[" + dummiesIndex + "].DummyEnum.Key", yourKey)

For the value you want to actually post you need to follow the complete path like above.

@Html.DefaultCombo("dummies[" + dummiesIndex + "].DummyEnum[" + dummyEnumIndex+ "]", "Value", "Text", Model.EnumList, enum)

Now MVC can remap your objects.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM