0

I have a view that contains quite many select elements (in the form of @Html.DropDownList or @Html.DropDownListFor). The problem is that they are arranged in a table-like manner and doubly-indexed (the number of rows and columns changes depending on the data).

It is only possible to use single-indexed properties/fields to bind to the selected value of the DropDownListFor helper, and the number of properties I'd need varies, so I wonder:

Is there an ASP.NET MVC way to get the selected values in the controller?

That is, I would now use jQuery to build some (maybe JSON) data to send to the controller manually. But first I'd like to know if there is something else I could try :)

Example project: View:

@model DropDownMatrixTest.Models.MatrixViewModel

@using (Html.BeginForm("Foo", "Home", FormMethod.Post))
{
  <table>
    <tbody>
      @for (int i = 0; i < 10; i++)
      {
        <tr>
          <td>@i-th row:</td>
          @for (int j = 0; j < 4; j++)
          {
            <td>
              @Html.DropDownListFor(m => m.SelectedValues[@i, @j],
                Model.Foos.Select(x => new SelectListItem
                {
                  Text = x.Text,
                  Value = x.Id.ToString()
                }))
            </td>
          }
        </tr>
      }
    </tbody>
  </table>
  <button type="submit">Submit</button>
}

ViewModel:

public class MatrixViewModel
{
  public IEnumerable<Foo> Foos { get; set; }
  public int[,] SelectedValues { get; set; } // I know this wouldn't work
}

Controller methods:

public ActionResult Index()
{
  MatrixViewModel vm = new MatrixViewModel
  {
    Foos = Enumerable.Range(1, 10).Select(x => new Foo { Id = x, Text = "Foo " + x })
  };
  return View(vm);
}

[HttpPost]
public ActionResult Foo(MatrixViewModel vm)
{
  // Here is where I'd like to get the selected data in some form
  return View("Index", vm);
}
InvisiblePanda
  • 1,589
  • 2
  • 16
  • 39

1 Answers1

1

Create view models to represent you table/matrix structure

public class CellVM
{
  public int SelectedValue { get; set; }
}
public class RowVM
{
    public RowVM()
    {
        Columns = new List<CellVM>();
    }   
    public RowVM(int columns)
    {
        Columns = new List<CellVM>();
        for(int i = 0; i < columns; i++)
        {
            Columns.Add(new CellVM());
        }
    }
    public List<CellVM> Columns { get; set; }
}
public class MatrixVM
{
    public MatrixVM()
    {
        Rows = new List<RowVM>();
    }
    public MatrixVM(int columns, int rows)
    {
        Rows = new List<RowVM>();
        for(int i = 0; i < rows; i++)
        {
            Rows.Add(new RowVM(columns));
        }
        // initialize collection with the required number of rows
    }
    public List<RowVM> Rows { get; set; }
    public IEnumerable<SelectListItem> Foos { get; set; }
}

In the GET method, initialize a new instance of MatrixVM and populate the SelectList

MatrixVM model = new MatrixVM(4, 4)
{
  Foos = Enumerable.Range(1, 10).Select(x => new SelectListItem(){ Value = x.ToString(), Text = "Foo " + x })
};
return View(model);

And in the view

@model MatrixVM
@using (Html.BeginForm())
{
  <table>
    <tbody>
      @for(int r = 0; r < Model.Rows.Count; r++)
      {
        <tr>
          @for(int c = 0; c < Model.Rows[r].Columns.Count; c++)
          {
            <td>
              @Html.DropDownListFor(m => m.Rows[r].Columns[c].SelectedValue, Model.Foos)
            </td>
          }
        </tr>
      }
    <tbody>
  </table>
  <input type="submit" />
}

Side note: The example creates a single SelectList in the controller which is efficient and suitable for a create view, but if your editing existing values, you will need to generate a new SelectList and set the Selected property in each iteration due to a limitation in DropDownListFor() when used in a loop

  • Hi Stephen, thanks for the great advice (again)! I'll definitely try this as it's way more elegant than using jQuery/other stuff. I would have tried using a string indexer next and parse it to get the coordinates, but that is also more tedious! I can't test it for the next few days, but I'm pretty sure it should work, so take my "Accept" ;) – InvisiblePanda Aug 28 '15 at 07:24
  • If I get time in an hour or so, I'll dump it into a DotNetFiddle so you can see how it works. –  Aug 28 '15 at 07:27
  • Oh, thanks but you don't need to do that, I understood the "how" (honestly I'm a little angry at myself for not thinking about breaking down the dimensions like this ;-)). Plus, I can't open DotNetFiddle at work anyway as it's blocked for some reason :/ – InvisiblePanda Aug 28 '15 at 07:34
  • I'll do it anyway because I have already spotted a couple of typo's in my answer which I want to correct :) –  Aug 28 '15 at 07:43
  • Done - refer [DotNetFiddle](https://dotnetfiddle.net/yX0JWw). And answer updated with a couple of corrections. –  Aug 28 '15 at 07:54
  • One remark: This doesn't really work, but it's due to [a bug](https://devio.wordpress.com/2013/06/27/dropdownlistfor-indexed-properties/) which is still not fixed it seems (some workarounds are mentioned in the link). What happens is that the data a user enters is passed correctly to the controller; but when creating the view model and initializing the selected values, they are ignored and the first items in the dropdowns are always selected. – InvisiblePanda Aug 29 '15 at 05:39
  • And its not actually a bug, just a limitation. It has been reported a number of times on CodePlex and dismissed by the MVC team. As an alternative to generating a new `SelectList` in each iteration (and setting the `SelectedValue` property, you can also use custom `EditorTemplates` for each type and pass the `SelectList` to the template as `additionalViewData` –  Aug 29 '15 at 05:44
  • Wow, I'm sorry, the side note has for some reason totally escaped me... it could have saved me a little searching :) Thanks again! – InvisiblePanda Aug 29 '15 at 05:46