Kobarin's Development Blog

C#やASP.NETなどについての記録です。

ASP.NET MVC で複数レコードを一括更新するフォーム

MVCのScaffoldilng機能は秀逸で、Index、Details、Create、Edit、DeleteのController+Viewをものの数秒で作り上げてくれます。
ただ、Create・Edit・Deleteいずれも1レコード単位の機能であり「一括変更」する機能はないため、自前で作る必要があります。
本稿では、特に利用頻度が多い思われるEdit(編集機能)の一括機能を取り上げていきたいと思います。


MVCではWebPagesに比べると簡単に実現できるのですが、実はちょっと癖があります。
ここでは、一般的に考えつく方法(IEnumerable + foreach)では出来ない事を示しつつ説明していくので、手っ取り早く方法を知りたい方はいきなり後半に飛んで下さい。

Controllerの記述

Controller側としては、こんな流れを想定します。

  • モデルの一覧表示
  • 受取側(HttpPost側)でforeachで1件ずつ更新処理
public ActionResult PostMultiRows()
{
  // 一覧の取得
  var products = from p in db.Products
                 select p;

  return View(products);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult PostMultiRows(IEnumerable<Product> products)
{
  MyRepository rp = new MyRepository(); // Productの取得や更新を行うレポジトリが既に作成されていると仮定

  // 1件ずつ更新
  foreach(var p in products)
  {
    rp.UpdateProduct(p);
  }

  return View("PostMultiRows");
}

Viewの記述

View側では、モデル一覧をバインドしつつ、EditorForで入力欄を用意します。
ここでは、簡略化するために、ProductId、Name、StandardPrice、ListPriceの4項目に絞っています。

@model IEnumerable<MvcDemo.Models.Product>

@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()

  <table class="table">

    @foreach (var item in Model)
    {
      <tr>
        <td>
          @Html.HiddenFor(m => item.ProductID)
          @Html.EditorFor(m => item.Name, new { htmlAttributes = new { @class = "form-control" } })
        </td>
        <td>
          @Html.EditorFor(m => item.StandardCost, new { htmlAttributes = new { @class = "form-control" } })
        </td>
        <td>
          @Html.EditorFor(m => item.ListPrice, new { htmlAttributes = new { @class = "form-control" } })
        </td>
      </tr>
    }
  </table>

  <div>
    <input type="submit" value="SUBMIT" class="btn btn-primary" />
  </div>
}

ひとまず完成。早速POSTしてみる。

簡単ですがひとまず出来上がりましたので、SUBMITボタンを押すと‥

オブジェクト参照がオブジェクト インスタンスに設定されていません

foreach(var p in products)

こんなエラーメッセージが現れるのではないでしょうか?
ASP.NETで最も良く見るエラーかもしれませんが、foreachの冒頭で発生していることから、うまく受け渡しができていない事がわかります。

foreachでは送信できない。for-loopが必要。

解決方法を言ってしまうと、View側ではforeachでなくforでループさせる必要があります。
当方の力不足のためハッキリした理由は説明できません(StackOverFlowで見つけただけ)ので、HTMLソースを見ての推測になりますが、
inputコントロール名が「[5].StandadCost」等となっている事から、おそらくループ内の各コントロールを識別可能になった事が原因ではないかと考えます。
また、これに伴い、IEnumerableではfor-loop指定ができないため、ViewモデルをIList<モデル名>に変更する必要もあります。

@model IList<MvcDemo.Models.Product>

@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()

  <table class="table">

    @for (int i = 0; i < Model.Count(); i++)
    {
      <tr>
        <td>
          @Html.HiddenFor(m => Model[i].ProductID)
          @Html.EditorFor(m => Model[i].Name, new { htmlAttributes = new { @class = "form-control" } })
        </td>
        <td>
          @Html.EditorFor(m => Model[i].StandardCost, new { htmlAttributes = new { @class = "form-control" } })
        </td>
        <td>
          @Html.EditorFor(m => Model[i].ListPrice, new { htmlAttributes = new { @class = "form-control" } })
        </td>
      </tr>
    }

  </table>

  <div>
    <input type="submit" value="SUBMIT" class="btn btn-primary" />
  </div>
}

これにより、更にContoller側にも変更が生じます。
IEnumerable型をList側に変換してViewに渡すようにして下さい。

public ActionResult PostMultiRows()
{
  var products = _rp.GetProducts(1, null).ToList();

  return View(products);
}

これで実行可能です。

いかがでしたでしょうか?
StackOverFlowでも質問している人が何人かいた事からも結構引っかかりやすいかもしれませんが、知ってしまえばかなり簡単だと思います。