3

I have this in a view:

  @{
    var i = 0;
  }
  @foreach (var field in Model.ConsentQuestions)
    {
      <div class="form-group">
        <div class="control-label col-md-8">
          @Html.Label(string.Format("ConsentQuestions[{0}].Key", i), field.Key)
          @Html.Hidden(string.Format("ConsentQuestions[{0}].Key", i), field.Key)
        </div>
        <p class="col-md-2">
          @Html.RadioButton(string.Format("ConsentQuestions[{0}].Value", i), true, htmlAttributes: new { id = string.Format("question{0}-true", i) })
          <label for="question@(i)-true">Yes</label>
          @Html.RadioButton(string.Format("ConsentQuestions[{0}].Value", i), false, htmlAttributes: new { id = string.Format("question{0}-false", i) })
          <label for="question@(i)-false">No</label>
        </p>
      </div>
      i++;
    }

Model.ConsentQuestions is an IEnumerable<KeyValuePair<string, bool>> (the reason for this is that the questions are user-definable). For whatever reason, the binder can't figure out this one out. Usually this sort of indexing works fine with collections (I am doing something similar with other IEnumerables with no issue). What's strange is if I breakpoint my validating method, it sees that there are the right number of items in ConsentQuestions, except each KVP is {"", false}.

I'd like to know how to correct the problem and get the values in the form to bind.

edit: I do have a seemingly working solution in using a class that inherits from DefaultModelBinder and then overrides GetPropertyValue to look right at controllerContext.HttpContext.Request.Form... while that's fine as far as it goes I'd still like to know why in this case it isn't working.

Casey
  • 3,307
  • 1
  • 26
  • 41
  • Have you tried passing a List>` instead and indexing with your `i` index, using `RadioButtonFor(m=>ConsentQuestions[i].Value)` syntax? That usually gets the indexed element binding right. – iCollect.it Ltd May 02 '14 at 19:59
  • Oh, one addendum: that should be m => m.ConsentQuestions[i].Value in your example. – Casey May 03 '14 at 01:34

2 Answers2

2

As mentioned in my comment (except for a typo), you need to use a List, not an IEnumerable (as that cannot be indexed at runtime by the binder) and use the For helpers on the index properties.

My TestModel looks like:

public class TestModel
{
    public List<KeyValuePair<string,bool>> ConsentQuestions { get; set; }
    public TestModel()
    {
        var consentQuestions = new List<KeyValuePair<string,bool>>();
        for (int i = 1; i <= 10; i++)
        {
            consentQuestions.Add(new KeyValuePair<string,bool>("Question " + i.ToString(), i % 2 == 0));
        }
        this.ConsentQuestions = consentQuestions;
    }
}

I then applied ...For version of the helpers on the indexed element properties:

View:

<div class="control-label col-md-8">
    @Html.DisplayFor(m=>m.ConsentQuestions[i].Key)
</div>
<p class="col-md-2">
    @Html.RadioButtonFor(m=>m.ConsentQuestions[i].Value, true, htmlAttributes: new { id = string.Format("question{0}-true", i) })
    <label for="question@(i)-true">Yes</label>
    @Html.RadioButtonFor(m => m.ConsentQuestions[i].Value, false, htmlAttributes: new { id = string.Format("question{0}-false", i) })
    <label for="question@(i)-false">No</label>
</p>

Note: I changed the HiddenFor to a DisplayFor to see the question (feel free to tweak).

Which works:

enter image description here

Followup:

I have been examining the output generated and the only difference is that using the first style of binding fails to match the existing value (the second one works). This applies to using a solid class or the KeyValuePair (makes no difference). It looks like the RadioButton binder is broken, but the RadionButtonFor is not:

@Html.RadioButton("ConsentQuestions[" + i.ToString() + "].Value", true, htmlAttributes: new { id = string.Format("question{0}-true", i) })
@Html.RadioButtonFor(m => m.ConsentQuestions[i].Value, false, htmlAttributes: new { id = string.Format("question{0}-false", i) })

Results in half working!:

enter image description here

This is obviously a little bizarre, as it should work the same, but the solution is still to just use the For versions of the helpers. Strongly typed code is always preferable to strings anyway.

iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
  • In your testing was there any difference with the keys? In my case neither was binding. – Casey May 05 '14 at 12:31
  • @emodendroket: sorry, I don't understand what you mean. Can you please clarify? – iCollect.it Ltd May 05 '14 at 14:58
  • `@Html.DisplayFor(m=>m.ConsentQuestions[i].Key)`... in my case, this was also failing to bind and so if the page failed the validation it would reload with all the labels blank. Did you have any issues with this? – Casey May 05 '14 at 16:25
  • @emodendroket: Can you please post your controller `Edit` actions etc so i can update my test project and see if there is another issue at work? – iCollect.it Ltd May 05 '14 at 17:03
1

Simply change your model to use a Dictionary (which is literally a list of KeyValuePairs). You won't even need to change your view.

So something like this:

public ActionResult AnswerQuestions(IEnumerable<KeyValuePair<string, bool>> ConsentQuestions)
{
    // Do stuff
}

Would be changed to this:

public ActionResult AnswerQuestions(Dictionary<string, bool> ConsentQuestions)
{
    // Do stuff
}

The main difference is that in addition to the properties that IEnumerable has, it also has .Comparer, .ContainsKey(), .ContainsValue(), .Count, .Keys, .TryGetValue(), and .Values.

Here's a full answer on why binding KeyValuePairs doesn't work.

Community
  • 1
  • 1
Pluto
  • 2,900
  • 27
  • 38
  • Strictly speaking, `Dictionary` implements `IEnumerable` (which means the current specification can accept dictionaries). Interesting point about the structs; I hadn't thought about that. – Casey Sep 02 '14 at 19:06