Reputation: 150
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>
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?
Upvotes: 4
Views: 1037
Reputation: 150
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>>>>>
.
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.
Upvotes: 3
Reputation: 326
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 :-)...)
Upvotes: 0
Reputation: 496
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:
select
element name
attribute value - it should contain full existing path.As I got from your code example the following.
DummyViewModel
view model class. It has property Dummies
.Dummy
class, that nested in DummyViewModel
as Dummies
. 2nd level dictionary.DummyEnum
enum class, that is in use at DummyEnum
values. Same names, different adjacent levels.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:
Dummies
type as a parameter.ActionResult SomeFromProcessingAction(DummyViewModel Dummies)
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.
Upvotes: 0