Bind Bools List Lists Dictionary with a strongly typed MVC view using checkboxes

I am using MVC 4, .Net 4 and Visual Studio 2012.

I am trying to use a rather complex model with one of my views, and I am having serious problems with its proper binding.

The model wraps a dictionary with integer keys and values, which are lists of bools lists.

Basically, the search was performed on the elements indicated by an integer, each element had several search conditions, and for each of these terms we have a list of results. I show the results on the page and check the box next to each result. For each result, the user will indicate whether they want some things done in the next step by checking the box.

At the moment, the checkboxes are displayed correctly, including predefined values โ€‹โ€‹from the controller, but when I click the submit button at the bottom of the form, I get this error:

Specified cast is not valid. Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. Exception Details: System.InvalidCastException: Specified cast is not valid. 

It seems to me that I have something to do with using the Dictionary, which, as I was told, does not work well as a model. I may have to switch to something else, but I would prefer if I did not have to. It looks like there might be an answer: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx or List of checkboxes for a complex type in asp.net mvc or How to bind a type parameter a dictionary for both GET and POST actions on ASP.NET MVC , but I found them after the whole question was written, and I still didn't understand, so maybe someone can give me a hand.

Here's the top of the stack trace:

 [InvalidCastException: Specified cast is not valid.] System.Web.Mvc.CollectionHelpers.ReplaceDictionaryImpl(IDictionary`2 dictionary, IEnumerable`1 newContents) +131 [TargetInvocationException: Exception has been thrown by the target of an invocation.] System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor) +0 System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments) +92 System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) +108 System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters) +19 System.Web.Mvc.CollectionHelpers.ReplaceDictionary(Type keyType, Type valueType, Object dictionary, Object newContents) +178 

Here is the model:

 public class AutoResolveModel { public Dictionary<int, List<List<bool>>> SelectedResults { get; set; } public AutoResolveModel() { SelectedResults = new Dictionary<int, List<List<bool>>>(); } } 

Since this may make a difference, here is the ViewBag.iidToData structure that contains the results that will be displayed:

 In the controller action: var iidToData = new Dictionary<int, List<ItemSearchResult>>(); ViewBag.iidToData = iidToData; Elsewhere: public class ItemSearchResult { public string C { get; set; } public string S { get; set; } public List<int> Ss { get; set; } public List<int> Ks { get; set; } } 

Here are some important parts from View with variable names that have been changed to protect the innocent:

 @model AutoResolveModel @{ string machineID; Submission subm; tblSignatures sig; ItemSearchResult result; var dc = new CloudDataContext(); } @using( Html.BeginForm( "MyAction", "MyController", new { p = (int?) ViewBag.l }, FormMethod.Post ) ) { foreach( KeyValuePair<int, List<ItemSearchResult>> kv in ViewBag.iidToData ) { <input type="hidden" name="@("SelectedResults[ " + kv.Key + " ].Key")" value="@kv.Key" /> ID = ( ... ).Single(); <h3>Inventory Item @ID</h3> for(int isr = 0; isr < kv.Value.Count(); isr++) { result = kv.Value[ isr ]; <h4>Searched for @result.S from @result.C</h4> <table border="0"> <tr><th>K</th><th>I</th><th>V</th><th>G</th><th>D</th><th>S</th><th>T</th></tr> @for( int i = 0; i < result.Ks.Count(); i++ ) { subm = ( ... ).FirstOrDefault(); try { sig = ( ... ).Single(); } catch { sig = null; } if( subm != null && subm.K != 0 ) { <tr> <td>@Html.CheckBoxFor(m => m.SelectedResults[kv.Key][isr][i])</td> <td>@result.Ks[ i ]</td> <td>@subm.i</td> <td>@subm.v</td> <td>@subm.g</td> <td>@subm.d</td> @if( sig != null ) { <td>@sig.S</td> <td>@sig.T</td> } else { <td>N/A</td> <td>N/A</td> } </tr> } } </table> } } <button type="submit">Search</button> } 
+4
source share
1 answer

OK I understood.

I tried using Tuple <int, List <List <bool โ†’> instead of the dictionary <int, List <List <bool โ†’>. This failed, apparently because Tuple does not have a constructor with 0 parameters.

Then I tried to use a custom class with two properties: int and List <List <bool โ†’. I got it to work after some grunts, and as soon as it worked, I was able to redesign it and make the dictionary work.

Here's the working version (same view model and iidToData as before):

 ... @{ string machineID; Submission subm; tblSignatures sig; ItemSearchResult result; int kvInd = 0; var dc = new CloudDataContext(); } ... foreach( KeyValuePair<int, List<ItemSearchResult>> kv in ViewBag.iidToData ) { ... <input type="hidden" name="@("Model.SelectedResults[" + kvInd + "].Key")" value="@kv.Key" /> for(int isr = 0; isr < kv.Value.Count(); isr++) { ... @if(result.Keytbls.Any()) { for( int i = 0; i < result.Keytbls.Count(); i++ ) { ... <td>@Html.CheckBox( "Model.SelectedResults[" + kvInd + "].Value[" + isr + "][" + i + "]", Model.SelectedResults[ kv.Key ][ isr ][ i ] )</td> ... } else { <tr><td><input type="hidden" name="@("Model.SelectedResults[" + kvInd + "].Value[" + isr + "]")" /></td></tr> } ... } kvInd++; } ... 

Thus, the index used to covertly enter the dictionary key is not a key, but an enumeration of KeyValue, 0th, 1st, 2nd, and so on. This is the same index that is used to indicate the value later.

This leads us to another funny part. There must be a Model.DictionaryName [enumerationIndex] for this flag. It indicates that we are setting the value for this indexed KeyValue pair.

In addition, the html input element created by this helper function is always true, and the second hidden input is always false. The checked attribute indicates whether the value of the input flag is sent to the binder by default or not, that is, whether it receives the value true, false or false. This is then correctly interpreted by the binder as the bool value.

Finally, hidden input in the else block at the end adds an empty <List <bool โ†’> list for entries that did not have matching search results. A .Value pair with an earlier .Key to specify the full KeyValue pair to be added to the dictionary. Then, when the binder sees Model.Dictionary [index] .Value [index], without seeing Model.Dictionary [index] .Value [index] [index], it makes an empty list, but does not add any values.

So it was unnecessarily complicated, but now, I hope others can use dictionaries with Collection values โ€‹โ€‹in their ViewModels.

+6
source

Source: https://habr.com/ru/post/1484530/


All Articles