Search code examples
c#asp.net-mvcmodel-bindingasp.net-mvc-helpers

Selected values in Html.DropDownListFor in a dynamically-sized "2D array layout"


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);
}

Solution

  • 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