WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

Mvc.dll のバージョン相違

by WebSurfer 2015年1月12日 16:06

Windows Update によって、知らないうちに System.Web.Mvc.dll のバージョンが変わり、突然 Visual Studio で MVC アプリケーションのビルドが通らなくなったという話を書きます。ちょっと古い話ですが、またあるかもしれませんので、備忘録として。

System.Web.Mvc.dll

2014 年の 5 月中旬の Windows Update に含まれる Microsoft ASP.NET MVC セキュリティ更新プログラム MS14-059 (KB2990942) によって、参照先(C:\Program Files\Microsoft ASP.NET\ASP.NET MVC 3\Assemblies\System.Web.Mvc.dll)の System.Web.Mvc.dll のバージョンが 3.0.0.0 から 3.0.0.1 に変わってしまいました。上の画像を参照ください。

Visual Studio 2010 の MVC3 インターネットアプリケーションのテンプレートを使って Web アプリを作ると、「ローカルコピー」が False になっているので(上の画像を参照・・・True にすると bin フォルダにコピーされる)、アプリは上に書いた参照先の .dll を参照しますが、プロジェクトファイル (.csproj) では 3.0.0.0 の指定のままなので、バージョン不一致でビルドが通らないと言うことのようです。

解決策は stckoverflow の記事 ASP.NET MVC security patch to version 3.0.0.1 breaks build [duplicate] にありますように、以下の手段を取るのがよさそうです。

  1. Visual Studio の「ソリューションエクスプラーラー」で、System.Web.Mvc の参照設定を一旦削除してからやり直す。

    蛇足ですが、手動でプロジェクトファイルを編集する方法もあるそうです。興味がありましたら、msdn ブログの記事 Visual Studio で現在開いてるプロジェクトファイル (.csproj, .vbproj) を編集する方法 を見てください。
  2. 「ローカルコピー」プロパティを False から True に変更する。(上の画像を参照。今後の Windows Update による影響を受けないようにするため)
  3. web.congfig の bindingRedirect 要素を以下のように修正する。
<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <dependentAssembly>
      <assemblyIdentity 
          name="System.Web.Mvc" 
          publicKeyToken="31bf3856ad364e35" />
      <bindingRedirect 
          oldVersion="1.0.0.0-3.0.0.0" 
          newVersion="3.0.0.1" />
    </dependentAssembly>
  </assemblyBinding>
</runtime>

なお、MVC4 でも System.Web.Mvc.dll のバージョンは 4.0.0.0 から 4.0.0.1 に変わっています(未確認ですが、MVC2, MVC5 も同様だと思います)。ただし、MVC4 の場合、参照設定と参照先の .dll のバージョン不一致の問題は出なかったので気がつきませんでした。

参考までに、なぜ MVC4 では問題が出なかったかを以下に書いておきます。

Visual Studio 2010 のインターネットアプリケーションテンプレートで MVC4 アプリを作ると、アプリケーションルート直下の packages フォルダに System.Web.Mvc.dll ほか必要な .dll がコピーされます。(NuGet によるパッケージ管理のため?)

コピーされる .dll のバージョンは 4.0.0.0 で、当然ですが参照設定もそのバージョンになります。

Visual Studio で System.Web.Mvc のプロパティを見るとわかりますが、「ローカルコピー」が True に設定されているので、ビルドする時に bin フォルダに .dll がコピーされます。

Windows Update によって packages フォルダの .dll は書き換えられることはないので、参照設定と参照先(bin フォルダの .dll)のバージョンの不一致は起こらないという仕組みです。

なお、Windows Update 後に新たに MVC4 プロジェクトを作っても、使われる System.Web.Mvc.dll は 4.0.0.0 のままとなります(自動的に最新バージョンが参照されることはない)ので注意してください。

具体的に言うと、C:\Program Files\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies の方の System.Web.Mvc.dll は 4.0.0.1 に更新されますが、C:\Program Files\Microsoft ASP.NET\ASP.NET MVC 4\Packages の方は 4.0.0.0 のままになり、プロジェクトの packages フォルダにコピーされるのは後者のフォルダにある 4.0.0.0 となるようです。

4.0.0.0 から 4.0.0.1 への更新が必要であれば、Visual Studio のテンプレートで Web アプリを作成後、NuGet のパッケージマネージャーを使って手動で行う(「NuGet パッケージの管理」を起動して、その中から「Microsoft ASP.NET MVC 4」を選択して更新をかける)必要があります。

Tags:

MVC

親子関係のあるデータの編集・削除

by WebSurfer 2014年12月22日 12:08

先に、(1) Entity Framework Code First の機能を利用して MVC4 アプリケーション用 SQL Server DB のテーブル作成、(2) Create アクションメソッドとビューを追加して作成したテーブルに親子関係のあるデータを登録する方法・・・という記事を書きました。

今回はそれに続いて Edit, Delete アクションメソッドとビューを追加し、先に追加した Create アクションメソッドで登録したデータの編集・削除を行う方法を書きます。(下の画像は Delete 操作のときのものです)

データの削除画面

先の記事 MVC4 EF Code First で書きましたように、Code First の機能を用いて生成した SQL Server DB の Parents, Children テーブルの間に外部キー制約が設定されています(Parents の Id ← Children の Parent_Id)。

なので、階層更新が必要になります。つまり、登録する場合は Parents テーブルに親レコードを INSERT した後 Children テーブルに子レコードを INSERT する、削除する場合は先に Children テーブルの関連する子レコードを全部 DELETE してから Parents テーブルの親レコードを DELETE するという操作が必要になります。

そのために Entity Framework 上で必要な操作としては、対象となるレコードの エンティティオブジェクトの状態 を、登録なら Added、編集なら Modified、削除なら Deleted としてマークし、DbContext.SaveChanges メソッド を適用すればよさそうです。

階層更新(上の例で言うと、INSERT, DELETE するときの順番)や、INSERT 時に Parents テーブルの親レコードの Id(IDENTITY 列)から値を取得して Children テーブルの子レコードの Parent_Id に設定するという操作は Entity Framework が面倒を見てくれるようです。(それを書いた公式文書が見つけられず、検証した結果だけ見てそう言っているので、100% の自信はないですけど・・・)

従って、プログラマが行うべきことで重要なのは、対象エンティティオブジェクトの状態を正しく Added, Modified, Deleted に設定してやると言うことになります。

具体的な方法は、文章で書くよりはコードを示した方がわかりやすいと思いますので、Edit, Delete 操作用のアクションメソッドと View のコードを下にアップしました。

自分が犯した失敗例や、必要な(と自分が思った)コメントも書いておきました。参考になれば幸いです。

Edit アクションメソッド

注意事項はコード内のコメントに書きましたので、それを参照してください。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Mvc4App2.Models;

namespace Mvc4App2.Controllers
{
  public class ParentChildController : Controller
  {
    private ParentChildContext db = new ParentChildContext();
        
    //・・・中略・・・

    //
    // GET: /ParentChild/Edit/5
    public ActionResult Edit(int id)
    {
      Parent parent = db.Parents.Find(id);
      if (parent == null)
      {
        return HttpNotFound();
      }
      return View(parent);
    }

    //
    // POST: /ParentChild/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(int id, Parent postedParent)
    {
      if (ModelState.IsValid)
      {
        // 以下のコードはダメ。
        // ポストされた Name の子レコードが新手に作られて
        // INSERT され、その Parent_Id が親の Id に設定さ
        // れる。既存の子レコードの Parent_Id は NULL に
        // 書き換えられる。

        //Parent parent = db.Parents.Find(id);
        //UpdateModel<Parent>(parent);
        //db.SaveChanges();

  
        // 以下のコードもダメ。
        // 親レコードしか書き換えられない。

        //db.Entry(postedParent).State = EntityState.Modified;
        //db.SaveChanges();


        // 以下のように子の方のエンティティ状態も 'Modified'
        // に設定すると親も子も期待通り更新される。

        for (int i = 0; i < postedParent.Children.Count; i++)
        {                    
          db.Entry(postedParent.Children[i]).State = 
                                       EntityState.Modified;
        }
        db.Entry(postedParent).State = EntityState.Modified;
        db.SaveChanges();

        return RedirectToAction("Index");
      }
      return View(postedParent);
    }

    //・・・中略・・・

  }
}

View (Edit.cshtml)

Parents, Children とも Id が隠しフィールド(@Html.HiddenFor)に設定されている点に注意してください。

コードの最後の方の @Scripts.Render("~/bundles/jqueryval") は入力検証用の jQuery ライブラリを登録するためのものです。これがないとクライアント側での検証はかかりませんので注意してください。

@model Mvc4App2.Models.Parent

@{
  ViewBag.Title = "Edit";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Edit</h2>

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

  <fieldset>
    <legend>Parent</legend>

    @Html.HiddenFor(model => model.Id)

    <div class="editor-label">
      @Html.LabelFor(model => model.Name)
    </div>
    <div class="editor-field">
      @Html.EditorFor(model => model.Name)
      @Html.ValidationMessageFor(model => model.Name)
    </div>

    <hr />

    @for (int i = 0; i < Model.Children.Count; i++)
    {       
      @Html.HiddenFor(model => model.Children[i].Id)
            
      <div class="editor-label">
        @Html.LabelFor(model => 
                        model.Children[i].Name)
      </div>
      <div class="editor-field">
        @Html.EditorFor(model => 
                        model.Children[i].Name)
        @Html.ValidationMessageFor(model => 
                        model.Children[i].Name)
      </div>
                
      <hr />
    }

    <p>
      <input type="submit" value="Save" />
    </p>
  </fieldset>
}

<div>
  @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
  @Scripts.Render("~/bundles/jqueryval")
}

Delete アクションメソッド

注意事項はコード内のコメントに書きましたので、それを参照してください。


    //・・・前略・・・

    //
    // GET: /ParentChild/Delete/5
    public ActionResult Delete(int id)
    {
      Parent parent = db.Parents.Find(id);
      if (parent == null)
      {
        return HttpNotFound();
      }
      return View(parent);
    }

    //
    // POST: /ParentChild/Delete/5
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public ActionResult DeleteConfirmed(int id)
    {
      // 以下のコードはダメ。
      // 子のデータががある場合、FK 制約に引っかかって
      // SqlException がスローされる。

      //Parent parent = db.Parents.Find(id);
      //db.Parents.Remove(parent);

            
      // 以下のコードもダメ。
      // 親と1 つ目の子レコードは削除されるが 2 つ目が残ってし
      // まう。(ただし残った子レコードの Parent_Id は NULL に
      // 書き換えられるので FK 制約には引っかからない。何故?)
      // Remove すると、その度 parent.Children.Count が一つ減っ
      // てしまう。そのため 1 つ目の子レコードを Remove した後
      // ループを抜けてしまい、2 つ目が Remove できないので、
      // db.SaveChanges() しても 2 つ目が残ってしまう。

      //Parent parent = db.Parents.Find(id);
      //for (int i = 0; i < parent.Children.Count; i++)
      //{
      //    db.Children.Remove(parent.Children[i]);
      //}
      //db.Parents.Remove(parent);            
      //db.SaveChanges();


      // 以下のように一旦 Child のコレクションを保持しておき、
      // それを使って Remove すれば OK。

      Parent parent = db.Parents.Find(id);

      List<Child> children = new List<Child>();
      foreach (Child child in parent.Children)
      {
        children.Add(child);
      }

      foreach (Child child in children)
      {
        db.Children.Remove(child);
      }
      db.Parents.Remove(parent);            
      db.SaveChanges();

      return RedirectToAction("Index");
    }

    protected override void Dispose(bool disposing)
    {
      db.Dispose();
      base.Dispose(disposing);
    }
  }
}

View (Delete.cshtml)

form 要素には action="/ParentChild/Delete/21" というように設定されます(21 は親レコードの Id)。なので、[Delete]ボタンをクリックして POST すると、アクションメソッド DeleteConfirmed(int id) の引数には親レコードの Id が渡されます。

@model Mvc4App2.Models.Parent

@{
  ViewBag.Title = "Delete";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<fieldset>
  <legend>Parent</legend>
  <div class="display-label">
     @Html.DisplayNameFor(model => model.Id)
  </div>
  <div class="display-field">
    @Html.DisplayFor(model => model.Id)
  </div>
  <div class="display-label">
     @Html.DisplayNameFor(model => model.Name)
  </div>
  <div class="display-field">
    @Html.DisplayFor(model => model.Name)
  </div>

  <hr />

  @for (int i = 0; i < Model.Children.Count; i++)
  {       
    <div class="display-label">
      @Html.DisplayNameFor(model => 
                      model.Children[i].Id)           
    </div>
    <div class="display-field">
      @Html.DisplayFor(model => 
                      model.Children[i].Id)
    </div>
        
    <div class="display-label">
      @Html.DisplayNameFor(model => 
                      model.Children[i].Name)           
    </div>
    <div class="display-field">
      @Html.DisplayFor(model => 
                      model.Children[i].Name)
    </div>
    <hr />
  }

</fieldset>
@using (Html.BeginForm()) {
  @Html.AntiForgeryToken()
  <p>
    <input type="submit" value="Delete" /> |
    @Html.ActionLink("Back to List", "Index")
  </p>
}

次の課題は、(1) 既存の親に属する子の全部または一部を DELETE、(2) 既存の親に子を追加・・・をどう実装するかですね。やる気が湧いてきたら書いてみます。(笑)

さらなる課題は、同時実行制御や、エラーの際のロールバックをどう実装するかでしょうか。そこのところは勉強不足でまだ見当さえついてません。先はずいぶん長そうです。(汗)

2016/9/12 追記:
Microsoft の文書「Code First の規約」に従って、Child クラスにナビゲーションプロパティと外部キープロパティを定義するとどのような影響があるかを別の記事「Code First で外部キープロパティの定義」に書きました。int 型の外部キープロパティを Child クラスに追加にしたのですが、それによる大きな影響は以下の 2 点です。他にも影響はありますが、詳しくは上にリンクを張った別記事を見てください。
  • 編集・更新: Child クラスに外部キープロパティを追加したので、編集結果を送信して更新をかける際、外部キープロパティの値も送信する必要がある。そうしないと参照整合性制約違反でエラー。
  • 削除: 外部キープロパティを int 型にしたので、外部キーフィールドが NULL 不可になり、連鎖削除が可能になる。

Tags: ,

MVC

親子関係のあるデータ登録

by WebSurfer 2014年12月21日 15:59

先の記事 MVC4 EF Code First では、Entity Framework Code First の機能を利用して、MVC4 インターネットアプリケーションの SQL Server データベースに Parents と Children という親子関係を持つ 2 つのテーブルを作成しました。

そのアプリケーションに Create アクションメソッドとビューを追加して、Parents テーブルと Children テーブルに親と子のデータを同時に登録する方法について書きます。

Create

クライアントから上のようなユーザー入力画面を使って送信されてくるデータ(上の例では "日本太郎"、"日本花子"、"日本一郎" という 3 つの文字列)を、Web サーバーで受け取って、Create アクションメソッドのパラメータにバインド(モデルバインディング)してやる必要があります。

モデルバインディングとデータアノテーション検証(クライアントサイドを含む)が正しく行われるためには、以下の 2 つの点を考慮する必要があります。

  1. input 要素の name 属性はデータがコレクションの場合 "prefix[index].Property" というパターンにする。
  2. クライアント側での検証に必要な属性が追加されるよう、ビューに EditorFor のような Html ヘルパーを使う。

詳しくは、先の記事「コレクションのデータアノテーション検証」に書きましたので、興味がありましたら読んでください。

上の 2 点を考慮に入れて作った Controller の Create アクションメソッドと View のコードを下にアップしましたので見てください。

プラウザから /ParentChild/Create を GET 要求すると一番上の画像のように表示されます。そこでデータを入力して[Create]ボタンをクリックすると、クライアント側で入力データが検証されたあと、 [HttpPost] 属性を付与した方の Create アクションメソッドに POST されます。

下の画像(Visual Stidio でのデバッグ画面)を見てください。POST されてきたデータは Create アクションメソッドの parent パラメータに正しくモデルバインディングされています。

Create

その際、付与したアノテーション属性(この記事の例では Parent, Child クラスの Name プロパティに付与した Required と StringLength)によってサーバー側で入力データの検証が行われ、その結果が ModelStateDictionary(Controller.ModelState プロパティで取得できます)に格納されます。

検証結果が OK(ModelState.IsValid が true)であれば、送信されてきたデータは SQL Server の Parents, Children テーブルに登録されます。

Controller

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Mvc4App2.Models;

namespace Mvc4App2.Controllers
{
  public class ParentChildController : Controller
  {
    private ParentChildContext db = new ParentChildContext();
        
    //・・・中略・・・

    //
    // GET: /ParentChild/Create
    // 引数 number は登録する子のレコード数。
    // 今回はとりあえずデフォルトで 2 としてみた。
    public ActionResult Create(int number = 2)
    {
      Parent parent = new Parent();
      for (int i = 0; i < number; i++)
      {
        Child child = new Child();
        parent.Children.Add(child);
      }
      return View(parent);
    }

    //
    // POST: /ParentChild/Create
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(Parent parent)
    {
      if (ModelState.IsValid)
      {
        db.Parents.Add(parent);

        // 2016/9/6 追記:
        // 以下のコードは不用でした。db.Parents.Add(parent) だけ
        // で親子両方の EntityState が Added になり、SaveChanges
        // メソッドで DB に親子とも INSERT されます。
        //for (int i = 0; i < parent.Children.Count; i++)
        //{
        //  db.Children.Add(parent.Children[i]);
        //}

        db.SaveChanges();
        return RedirectToAction("Index");
      }
      return View(parent);
    }

    //・・・中略・・・
  }
}

今回は簡略化のため、とりあえず、親 : 子 = 1 : 2 で固定としました。 1 : n で n をユーザーが設定できるようにする方法も別途書く予定です。

SQL Server の Parents テーブルの Id 列と Children テーブルの Parent_Id 列には外部キー制約が設けられていますので、先に Parents に親レコードを INSERT してから Children に子レコードを INSERT する必要があります(つまり階層更新が必要)。また、Id 列は IDENTITY なので INSERT 操作が完了するまで値が分からないという問題もあります。そのあたりは、仕組みは不明ですが、Entity Framework がうまくやってくれるようで、上のコードで問題なく Create できます。

View

@model Mvc4App2.Models.Parent

@{
  ViewBag.Title = "Create";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Create</h2>

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

  <fieldset>
    <legend>Book</legend>

    <div class="editor-label">
      @Html.LabelFor(model => model.Name)
    </div>
    <div class="editor-field">
      @Html.EditorFor(model => model.Name)
      @Html.ValidationMessageFor(model => model.Name)
    </div>

    <hr />
    
    @for (int i = 0; i < Model.Children.Count; i++)
    {       
      <div class="editor-label">
        @Html.LabelFor(model => model.Children[i].Name)
      </div>
      <div class="editor-field">
        @Html.EditorFor(model => model.Children[i].Name)
        @Html.ValidationMessageFor(model => 
                            model.Children[i].Name)
      </div>

      <hr />
    }

    <p>
      <input type="submit" value="Create" />
    </p>
  </fieldset>
}

<div>
  @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
  @Scripts.Render("~/bundles/jqueryval")
}

EditorFor(model => model.Name) からは name="Name"、EditorFor(model => model.Children[i].Name) からは name="Children[i].Name"(i は連番)という name 属性が生成されます。

SQL Server の Parents, Children テーブルで、Id 列はいずれも IDENTITY となっています。ユーザー入力は不要なのでその入力用の EditorFor ヘルパーは設けていません。

コードの最後の方の @Scripts.Render("~/bundles/jqueryval") はクライアント側でのユーザー入力検証用の jQuery ライブラリを登録するためのものです。これがないとクライアント側での検証はかかりませんので注意してください。


以上、とりあえず Create するまでを書きました。後日、別途、Edit および Delete する方法も書く予定です。階層更新は Delete がちょっと面倒な気がします。

Tags:

MVC

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar