5
votes

I'm using MVC 4, .Net 4, and Visual Studio 2012.

I'm trying to use a fairly complex model with one of my views, and I'm having serious trouble getting it to bind properly.

The model wraps a Dictionary with integer Keys, and Values that are Lists of Lists of bools.

Basically, a search was done on items indicated by the integer, each item had several search terms, and for each of those terms we have a list of results. I display the results on a page, and have a checkbox next to each result. For each result, the user will indicate whether they want some stuff done by the next Action by checking the box.

At the moment, the checkboxes display properly, including the preset values from the controller, but when I press 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 appears to me to have something to do with using a Dictionary, which I'm told doesn't work well as a model. I may have to change to something else, but I'd rather not unless I absolutely have to. Seems like there might be an answer here somewhere: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx, or Checkbox list for complex type in asp.net mvc, or How to bind Dictionary type parameter for both GET and POST action on ASP.NET MVC, but I found those after I had the question all written up and I haven't figured it out yet, so maybe somebody 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's the model:

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

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

Since it might be relevant, here's the structure of ViewBag.iidToData, which holds the results to 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's some relevant parts from the View with variable names 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>
}
2
After reviewing the links I included above, as well as hanselman.com/blog/…, I made a couple changes: Before the loop: int kvInd = 0; Changed: <input type="hidden" name="@("SelectedResults[ " + kv.Key + " ].Key")" value="@kv.Key" /> to: <input type="hidden" name="@("SelectedResults[ " + kvInd + " ].Key")" value="@kv.Key" />DCShannon
And: <td>@Html.CheckBoxFor(m => m.SelectedResults[kv.Key][isr][i])</td> To: <td> <input type="checkbox" name="@("SelectedResults[ " + kvInd + " ].Value[ " + isr + " ][ " + i + " ]")" value="@Model.SelectedResults[kv.Key][isr][i]" /> <input type="hidden" name="@("SelectedResults[ " + kvInd + " ].Value[ " + isr + " ][ " + i + " ]")" value="false" /> </td> But I still get the same error. Darn.DCShannon

2 Answers

7
votes

Alright, I got it.

I tried using a Tuple<int, List<List<bool>>> instead of a Dictionary<int, List<List<bool>>>. That failed, apparently because the Tuple doesn't have a 0-parameter constructor.

Then, I tried using a custom class that had two properties, an int and a List<List<bool>>. I got that to work after some fiddling, and once that worked I was able to reverse engineer it and get the Dictionary to 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++;

}

...

So, the index used on the hidden input for the dictionary key isn't the key, but is instead an enumeration of the KeyValue pairs, 0th one, 1st one, 2nd one, so on. This is the same index used to indicate the value later on.

That leads us to another funny part. The name for the checkbox needs to have Model.DictionaryName[enumerationIndex].Value in it to indicate that we are setting the value for that indexed KeyValue pair.

Also, the html input element produced by that helper function always has a value of true, and the second hidden input always has a value of false. The "checked" attribute indicates whether the value of the input checkbox is sent to the default binder or not, i.e. whether it gets the value of "true, false" or just "false". This is then properly interpreted by the binder as a bool value.

Finally, the hidden input in the else block at the end adds an empty List<List<bool>> for entries that had no matching search results. The .Value pairs with the earlier .Key to indicate a full KeyValue pair to be added to the dictionary. Then, when the binder sees Model.Dictionary[index].Value[index] without ever seeing Model.Dictionary[index].Value[index][index], it makes an empty list but doesn't add any values.

So that was unnecessarily complicated, but now hopefully others can use Dictionaries with Collection values in their ViewModels.

0
votes

I had to do something like this, and I had to do with the dictionary value in an array of decimals (Dictionary< string, decimal[] >). If it helps, I did like this:

@foreach (var kvp in Model.MyDictionary)
{
    <tr>
        <td>@kvp.Key</td>
        @for (int i = 0; i < kvp.Value.Count(); i++)
        {
            <td>
                <input type="text" name="@("MyDictionary[" + kvp.Key + "]")" value="@kvp.Value[i]" />
            </td>
        }
    </tr>
}