ASP.NET MVC

既定のスキャフォールディング テンプレートをオーバーライドする

Jonathan Waldman

コード サンプルのダウンロード

データ ストアに対して "作成 (Create)"、"取得 (Retrieve)"、"更新 (Update)"、"削除 (Delete)" を行うルーチンを作成するという日常のタスクは、"CRUD" というわかりやすい略語で表されます。マイクロソフトは、T4 テンプレートによって動作する便利なスキャフォールディング エンジンを提供し、Entity Framework を使用する ASP.NET MVC アプリケーションのモデルに、基本的な CRUD コントローラーとビューを自動作成できるようにしています (Entity Framework のスキャフォールディングを現時点では利用できない WebAPI や MVC もあります)。

スキャフォールディングは、わかりやすく、使いやすいページを生成し、開発者が CRUD ページの構築に関わる単調な作業を行わないで済むようにします。ただし、スキャフォールディングによって得られるのはこのように限られた機能だけなので、結局、生成したコントローラー ロジックやビューをニーズに合わせて微調整することになります。

スキャフォールディングは一方向のプロセスなので、調整にはリスクが伴います。モデルに変更を反映するためにコントローラーとビューのスキャフォールディングをやり直すには、加えた調整を上書きするしかありません。したがって、どのモジュールをカスタマイズしたかを慎重に追跡して、スキャフォールディングを安全にやり直せるモデルと、やり直せないモデルを認識する必要があります。

チーム環境では、このような注意を強制するのが困難です。さらに、編集コントローラーは、編集ビューにほとんどのモデル プロパティを表示するため、機密情報が公開されるおそれがあります。また、編集コントローラーは盲目的にモデル バインディングを行って、ビューが送信したプロパティをすべて保存するため、mass-assignment 攻撃のリスクが高まります。

今回は、ASP.NET MVC Entity Framework CRUD スキャフォールディング サブシステムをサポートする T4 テンプレートを、プロジェクトに合わせてカスタマイズする方法について取り上げます。その過程で、コントローラーの Create ポストバック ハンドラーと Edit ポストバック ハンドラーを拡張して、ポストバック モデル バインディングとデータ ストアの保存の間に独自のコードを挿入できるようにする方法を示します。

また、mass-assignment 攻撃の懸念に対処するため、カスタム属性を作成して、モデル プロパティを保存すべきかどうかを完全に制御できるようにします。その後、もう 1 つカスタム属性を追加して、編集ビューでプロパティが読み取り専用のラベルとして表示されるようにします。

最終的には、アプリケーションが攻撃を受ける側面を減らしながら、CRUD ページの制御と、モデルを表示および保存する方法の制御をこれまでにないレベルで行えるようになります。特にすばらしいのは、ASP.NET MVC プロジェクトのすべてのモデルにこの手法を流用できる点と、モデルが変化するたびににコントローラーやビューを安全に再生成できる点です。

プロジェクトのセットアップ

今回扱うソリューションは、Visual Studio Ultimate 2013、ASP.NET MVC 5、Entity Framework 6、および C# を使用して開発しています (取り上げたテクニックは、Visual Studio Professional/Premium 2013、Visual Studio Express 2013 for Web、および Visual Basic .NET にも適用できます)。また、ダウンロード用に 2 つのソリューションを用意しました。1 つはベースラインとなるソリューションで、作業中のプロジェクトで使い始めることができ、ここで紹介するテクニックを手動で実装します。もう 1 つは完全なソリューションで、ここで取り上げたすべての強化点を含んでいます。

どちらのソリューションにも 3 つのプロジェクトが含まれています。ASP.NET MVC Web サイトのプロジェクト、エンティティ モデルと T4 スキャフォールディング機能のプロジェクト、およびデータ コンテキストのプロジェクトの 3 つです。これらのソリューションのデータ コンテキストは、SQL Server Express データベースを指します。また、前述の依存関係に加え、NuGet を使用して Bootstrap を追加し、スキャフォールディングされたビューのテーマを設定します。

スキャフォールディング サブシステムは、Visual Studio のセットアップ中に [Microsoft Web Developer Tools] オプションを選択するとインストールされます。次回の Visual Studio Service Pack では、スキャフォールディング ファイルが自動的に更新されるようになります。Visual Studio Service Pack のリリースとリリースの間に提供されているスキャフォールディング サブシステムのアップデートを取得するには、最新の Microsoft Web プラットフォーム インストーラーを bit.ly/1g42AhP からダウンロードしてください。

今回のコード サンプルを使用できない場合は、最新バージョンを使用していることを確認し、ReadMe.txt ファイルをお読みください。コード サンプルは、必要に応じて更新します。

ビジネス ルールを定義する

CRUD ビューの生成にかかわる完全なワークフローを示すため、また要点に集中するため、ここでは Product という非常に簡単なエンティティ モデルを操作します。

public class Product
{
  public int ProductId { get; set; }
  public string Description { get; set; }
  public DateTime? CreatedDate { get; set; }
  public DateTime? ModifiedDate { get; set; }
}

原則として、MVC は ProductId が主キーであることは理解しますが、CreatedDate プロパティと ModifiedDate プロパティに特殊な要件があることについては認識しません。名前のとおり、CreatedDate は、(ProductId によって表される) 当該製品がデータベースに挿入された時点を示します。また、ModifiedDate は、最後に変更された時点を示します (UTC 時刻値を使用する予定です)。

ModifiedDate 値は、編集ビューに読み取り専用のテキストとして表示します (レコードが一度も変更されたことがなければ、ModifiedDate と CreatedDate は等しくなります)。CreatedDate はどビューにも表示しません。また、これらの日付値をユーザーが入力、変更できないようにするため、作成ビューまたは編集ビューで日付値の入力を収集するフォーム コントロールはすべてレンダリングしません。

これらの値が攻撃に耐えられるように、優秀なハッカーが値をフォーム フィールドやクエリ文字列値として入力できたとしても、値がポストバックに保存されないようにします。このようなビジネス ロジック層のルールを考慮して、データベースには、それらの列の値を保存する役割を与えません (たとえば、トリガーを作成したり、テーブル列の定義ロジックを埋め込んだりしません)。

CRUD スキャフォールディング ワークフローを確認する

まず、既定のスキャフォールディングの機能を調べます。コントローラーを追加するために、Web プロジェクトの Controllers フォルダーを右クリックして、[コントローラーの追加] を選択します。これにより、[スキャフォールディングの追加] ダイアログ ボックスが表示されます (図 1 参照)。

The MVC 5 Add Scaffold Dialog
図 1 MVC 5 の [スキャフォールディングの追加] ダイアログ ボックス

モデルの CRUD コントローラーとビューのスキャフォールディングを行うため、[Entity Framework を使用した、ビューがある MVC 5 コントローラー] を選択します。選択したら、[追加] をクリックします。ここで表示されるダイアログ ボックスには、最終的には T4 テンプレートのパラメーターに変換される多くのオプションがあります (図 2 参照)。

The Add Controller Dialog
図 2 [コントローラーの追加] ダイアログ ボックス

[コントローラー名] ボックスに「ProductController」と入力します。非同期操作は今回対象外なので、[非同期コントローラー アクションを使用します] チェック ボックスはオフにします。次に、[モデル クラス] で [Product] を選択します。Entity Framework を使用しているため、データ コンテキスト クラスが必要です。System.Data.Entity.DbContext から継承するクラスがドロップダウン メニューに表示されるので、ソリューションで複数のデータベース コンテキストを使用する場合は適切なクラスを選択します。[ビュー] の下では、[ビューの生成] チェック ボックスと [レイアウト ページの使用] チェック ボックスをオンにします。[レイアウト ページの使用] チェック ボックスの下にあるテキスト ボックスには何も入力しません。

[追加] をクリックすると、スキャフォールディングのために数個の T4 テンプレートが変換されます。このプロセスによって、Web プロジェクトの Controllers フォルダーに書き込まれるコントローラーのコード (ProductController.cs) が生成されます。また、Web プロジェクトの Views フォルダーに書き込まれる 5 個のビュー (Create.cshtml、Delete.cshtml、Details.cshtml、Edit.cshtml、および Index.cshtml) も生成されます。これで、Product エンティティのデータを管理するために必要なコントローラーとすべての CRUD ビューが揃いました。これらの Web ページを、インデックス ビューからすぐに使い始めることができます。

CRUD ページの外観や動作は、プロジェクトのいずれのモデルとも同じにすることをお勧めします。CRUD ページのスキャフォールディングに T4 テンプレートを使用すると、この一貫性を保つことができます。つまり、コントローラーとビューを直接変更するという誘惑に打ち勝たなくてはなりません。直接変更する代わりに、それらを生成する T4 テンプレートを変更します。スキャフォールディングが行われたファイルを、変更の必要がなく、使用可能な状態にするために、このことを実践してください。

コントローラーの欠点を調べる

スキャフォールディング サブシステムのおかげで比較的すぐに実行し始めることが可能ですが、生成されるコントローラーにはいくつかの欠点があります。この欠点を補うために、いくつか改善を施す方法をこれから説明します。作成と編集を扱う、スキャフォールディングが行われたコントローラー アクションのメソッドを以下に示します (図 3 参照)。

図 3 作成と編集を扱う、スキャフォールディングが行われたコントローラー アクションのメソッド

public ActionResult Create(
  [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] 
  Product product)
{
  if (ModelState.IsValid)
  {
    db.Products.Add(product);
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  return View(product);
}
public ActionResult Edit(
  [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] 
  Product product)
{
  if (ModelState.IsValid)
  {
    db.Entry(product).State = EntityState.Modified;
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  return View(product);
}

各メソッドの Bind 属性は、Product モデルのすべてのプロパティを明示的に含んでいます。MVC コントローラー モデルが、ポストバック後にすべてのモデル プロパティをバインドした状況を "mass-assignment" と呼びます。これは "オーバーポスティング" とも呼ばれ、セキュリティに深刻な脆弱性がもたらされます。その後データベース コンテキストで SaveChanges が呼び出されるため、ハッカーによってこの脆弱性が悪用される可能性があります。SaveChanges が呼び出されることにより、モデルがデータ ストアに保存されます。MVC 5 の CRUD スキャフォールディング システムで使用されるコントローラー テンプレートは、Create アクション ポストバック メソッドと Edit アクション ポストバック メソッドに、既定で mass-assignment コードを生成するようになっています。

作成ビューまたは編集ビューにレンダリングされないよう、モデルの特定のプロパティを装飾した場合も、mass-assignment が問題を引き起こします。そのプロパティは、モデル バインディングの後 null に設定されます (生成されたビューに、スキャフォールディングされたプロパティをレンダリングすべきかどうかを指定できる属性を示した、「CRUD ビューにプロパティが表示されないようにするために属性を使用する」を参照してください)。説明のため、まずは以下のように、Product モデルに 2 つの属性を追加します。

public class Product
{
  public int ProductId { get; set; }
  public string Description { get; set; }
  [ScaffoldColumn(false)]
  public DateTime? CreatedDate { get; set; }
  [Editable(false)]
  public DateTime? ModifiedDate { get; set; }
}

[コントローラーの追加] ダイアログ ボックスを使用して、前述のとおりにスキャフォールディングのプロセスを再実行したら、[Scaffold(false)] 属性によって、CreatedDate がどのビューにも表示されなくなります。[Editable(false)] 属性は、ModifiedDate が削除ビュー、詳細ビュー、およびインデックス ビューに表示されるようにしますが、作成ビューまたは編集ビューには表示されないようにします。プロパティが作成ビューまたは編集ビューにレンダリングされないと、そのプロパティはポストバックの HTTP 要求ストリームに使用されません。

これは問題になります。MVC によって動作する CRUD ページのモデル プロパティに値を代入する最後のチャンスはポストバック中だからです。そのため、プロパティのポストバックの値が null の場合、その null 値がモデル バインディングされます。すると、データ コンテキスト オブジェクトで SaveChanges が実行されたとき、データ ストアにモデルが保存されます。これが Edit ポストバック アクション メソッドで実行されると、そのプロパティは null 値に置き換えられます。これにより、データ ストアから現在の値が事実上削除されます。

サンプルでは、データ ストアの CreatedDate に保存されている値が失われます。実際、編集ビューにレンダリングされないプロパティによって、データ ストアの値が null で上書きされます。モデル プロパティかデータ ストアが null 値の代入を許可しないと、ポストバックでエラーが発生します。この欠点を修正するには、コントローラーを生成する役割を持つ T4 テンプレートを変更します。

スキャフォールディング テンプレートをオーバーライドする

コントローラーとビューがスキャフォールディングされる方法を変更するには、それらを生成する T4 テンプレートを変更する必要があります。すべての Visual Studio プロジェクトのスキャフォールディングにグローバルに影響するように、オリジナルのテンプレートを変更することも、コピーが含まれるプロジェクトのみに影響するように、T4 テンプレートのプロジェクト固有のコピーを変更することも可能です。ここでは、後者を実行します。

オリジナルの T4 スキャフォールディング テンプレートは、%programfiles%\Microsoft Visual Studio 12.0\Common7\IDE\Extensions\Microsoft\Web\Mvc\Scaffolding\Templates フォルダーにあります (このテンプレートは、いくつかの .NET アセンブリによっては、%programfiles%\Microsoft Visual Studio 12.0\Common7\IDE\Extensions\Microsoft\Web Tools\Scaffolding フォルダーにあります)。今回は、Entity Framework CRUD コントローラーおよびビューをスキャフォールディングする特定のテンプレートを操作します。図 4 にそれらのテンプレートをまとめます。

図 4 Entity Framework CRUD コントローラーおよびビューをスキャフォールディングする T4 テンプレート

スキャフォールディング テンプレートのサブフォルダー名

テンプレート ファイル名

(.cs は C#、.vb は Visual Basic .NET)

生成されるファイル

(.cs は C#、.vb は Visual Basic .NET)

MvcControllerWithContext

Controller.cs.t4

Controller.vb.t4

Controller.cs

Controller.vb

MvcView

Create.cs.t4

Create.vb.t4

Create.cshtml

Create.vbhtml

MvcView

Delete.cs.t4

Delete.vb.t4

Delete.cshtml

Delete.vbhtml

MvcView

Details.cs.t4

Details.vb.t4

Details.cshtml

Details.vbhtml

MvcView

Edit.cs.t4

Edit.vb.t4

Edit.cshtml

Edit.vbhtml

MvcView

Index.cshtml

Index.vbhtml

Index.cshtml

Index.vbhtml

プロジェクト固有のテンプレートを作成するには、オリジナルの T4 スキャフォールディング フォルダーから、CodeTemplates (この名前のとおりです) という ASP.NET MVC Web プロジェクト内のフォルダーに上書きするファイルをコピーします。原則として、スキャフォールディング サブシステムは、まず MVC プロジェクトの CodeTemplates フォルダーから一致するテンプレートを探します。

これが適切に機能するように、オリジナルのテンプレート フォルダーにある固有のサブフォルダー名とファイル名を、明示的に複製する必要があります。今回は、Entity Framework CRUD スキャフォールディング サブシステムから、上書きする T4 ファイルをコピーしました。今回の Web プロジェクトの CodeTemplates を図 5 に示します。

The Web Project’s CodeTemplates
図 5 Web プロジェクトの CodeTemplates

また、Imports.include.t4 と、ModelMetadataFunctions.cs.include.t4 もコピーしました。プロジェクトには、ビューをスキャフォールディングするために、これらのファイルが必要です。また、ファイルの C# (.cs) バージョンのみをコピーしました (Visual Basic .NET を使用されている方は、名前に .vb が含まれているファイルをコピーしてください)。スキャフォールディング サブシステムは、このようなグローバルではないプロジェクト固有のファイルを変換します。

Create アクション メソッドと Edit アクション メソッドを拡張する

これで、プロジェクト固有の T4 テンプレートに必要に応じて変更を加えられるようになりました。まず、コントローラーの Create アクション メソッドと Edit アクション メソッドを拡張し、保存される前にモデルを調査して変更できるようにします。テンプレートによって生成されるコードをできる限り汎用にするため、モデル固有のロジックはテンプレートに追加しません。代わりに、モデルにバインドされる外部関数を呼び出します。このようにすると、コントローラーの Create と Edit は、モデルでポリモーフィズムをシミュレーションしながら拡張されます。この目的を達成するため、以下のように、IControllerHooks というインターフェイスを作成します。

namespace JW_ScaffoldEnhancement.Models
{
  public interface IControllerHooks
  {
    void OnCreate();
    void OnEdit();
  }
}

次に、(CodeTemplates\MVCControllerWithContext フォルダーにある) Controller.cs.t4 テンプレートを変更し、モデルが IControllerHooks を実装したら、Create ポストバック アクション メソッドと Edit ポストバック アクション メソッドによってモデルの OnCreate メソッドと OnEdit メソッドがそれぞれ呼び出されるようにします。コントローラーの Create アクション ポストバック メソッドを図 6 に、Edit アクション ポストバック メソッドを 図 7 に示します。

図 6 拡張後のコントローラーの Create アクション ポストバック メソッド

public ActionResult Create(
  [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] 
  Product product)
{
  if (ModelState.IsValid)
  {
    if (product is IControllerHooks) { 
      ((IControllerHooks)product).OnCreate(); 
    }
    db.Products.Add(product);
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  return View(product);
}

図 7 拡張後のコントローラーの Edit アクション ポストバック メソッド

public ActionResult Edit(
  [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] 
  Product product)
{
  if (ModelState.IsValid)
  {
    if (product is IControllerHooks) { 
      ((IControllerHooks)product).OnEdit(); 
    }
    db.Entry(product).State = EntityState.Modified;
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  return View(product);
}

ここで、IControllerHooks を実装するように Product クラスを変更します。その後、コントローラーが OnCreate と OnEdit を呼び出すときに実行するコードを追加します。新しい Product モデル クラスを図 8 に示します。

図 8 コントローラーを拡張するために IControllerHooks を実装する Product モデル

public class Product : IControllerHooks
{
  public int ProductId { get; set; }
  public string Description { get; set; }
  public DateTime? CreatedDate { get; set; }
  public DateTime? ModifiedDate { get; set; }
  public void OnCreate()
  {
    this.CreatedDate = DateTime.UtcNow;
    this.ModifiedDate = this.CreatedDate;
  }
  public void OnEdit()
  {
    this.ModifiedDate = DateTime.UtcNow;
  }
}

確かに、この "拡張" ロジックを実装する方法はたくさんあります。ですがこのように、コントローラー テンプレートの Create メソッドと Edit メソッドを 1 行変更することで、モデル バインディングの後、保存される前に、Product モデル インスタンスを変更することが可能になります。さらに、作成ビューおよび編集ビューに公開されないモデル プロパティの値を設定することもできます。

モデルの OnEdit 関数は、CreatedDate の値を設定していないことがわかります。CreatedDate が編集ビューにレンダリングされない場合、コントローラーの Edit アクション メソッドが SaveChanges を呼び出したとき、CreatedDate は、Edit アクション メソッドがモデルを保存した後に null 値で上書きされます。これを防ぐため、コントローラー テンプレートにさらに変更を加える必要があります。

Edit アクション メソッドを強化する

mass-assignment に関連する問題のいくつかについては既に説明しました。モデル バインディングの動作を変更する方法の 1 つは、Bind 属性を変更して、バインドすべきでないプロパティを除外することです。ところが実際には、このアプローチでもデータ ストアに null 値を書き込むことになってしまいます。より適切な手法を取るにはプログラミングを追加で行う必要がありますが、苦労に見合う結果を得ることができます。

データベース コンテキストにモデルをアタッチするために、Entity Framework の Attach メソッドを使用します。こうすれば、エンティティのエントリを追跡して、IsModified プロパティを必要に応じて設定できるようになります。このことを実証するために、JW_ScaffoldEnhancement.Models プロジェクトに CustomAttributes.cs という新しいクラス モジュールを作成します (図 9 参照)。

図 9 新しいクラス モジュールの CustomAttributes.cs

using System;
namespace JW_ScaffoldEnhancement.Models
{ 
  public class PersistPropertyOnEdit : Attribute
  {
    public readonly bool PersistPostbackDataFlag;
    public PersistPropertyOnEdit(bool persistPostbackDataFlag)
    {
      this.PersistPostbackDataFlag = persistPostbackDataFlag;
    }
  }
}

この属性は、データ ストアに保存されないようにする編集ビューのプロパティを示すために使用します (修飾されていないプロパティには、暗黙の属性 [PersistPropertyOnEdit(true)] が設定されます)。CreatedDate プロパティが保存されないように、Product モデルの CreatedDate プロパティにのみ新しい属性を追加します。新しく修飾したモデル クラスを以下に示します。

public class Product : IControllerHooks
{
  public int ProductId { get; set; }
  public string Description { get; set; }
  [PersistPropertyOnEdit(false)]
  public DateTime? CreatedDate { get; set; }
  public DateTime? ModifiedDate { get; set; }
}

ここで、新しい属性が使用可能になるように Controller.cs.t4 テンプレートを変更する必要があります。T4 テンプレートを強化する際は、テンプレートの中とテンプレートの外のどちらに変更を加えてもかまいません。ただし、サードパーティのテンプレート エディター ツールを使用している場合を除き、外部のコード モジュールにできる限り多くのコードを配置することをお勧めします。その結果、(T4 のマークアップが散らばったものではなく) 純粋な C# の環境ができあがるため、コードに集中することができます。また、テストも行いやすくなり、広範囲にわたるテスト ハーネスの作業に独自の機能を組み込むことが可能になります。さらに、T4 スキャフォールディングからアセンブリが参照される方法におけるいくつかの欠点が理由となり、すべての設定を行う際の技術的な問題がほとんど発生しません。

Models プロジェクトには、GetPropertyIsModifiedList というパブリック関数があり、List<String> を返します。List<String> は、渡されるアセンブリと型に IsModified 設定を生成するために、反復処理できます。Controller.cs.t4 におけるこのコードを図 10 に示します。

T4 Template Code Used To Generate an Improved Controller Edit Postback Handler
図 10 強化後のコントローラーの Edit ポストバック ハンドラーを生成するのに使用する T4 テンプレート コード

図 11 の GetPropertyIsModifiedList では、指定したモデルのプロパティにアクセスするためにリフレクションを使用しています。その後、プロパティで反復処理を行って、どのプロパティが PersistPropertyOnEdit 属性で修飾されているか判断します。モデルのプロパティの大半を保存するのが望ましいので、既定でプロパティの IsModified 値を true に設定するようテンプレート コードを構成しました。こうすると、保存しないプロパティに [PersistPropertyOnEdit(false)] を追加するだけで済みます。

図 11 モデル プロジェクトの ScaffoldFunctions.GetPropertyIsModifiedList 静的関数

static public List<string> GetPropertyIsModifiedList(string ModelNamespace, 
  string ModelTypeName, 
  string ModelVariable)
{
  List<string> OutputList = new List<string>();
  // Get the properties of the model object
  string aqn = Assembly.CreateQualifiedName(ModelNamespace + 
    ", Version=1.0.0.0,
    Culture=neutral, PublicKeyToken=null", ModelNamespace + "." + 
    ModelTypeName);
  // Get a Type object based on the Assembly Qualified Name
  Type typeModel = Type.GetType(aqn);
  // Get the properties of the type
  PropertyInfo[] typeModelProperties = typeModel.GetProperties();
  PersistPropertyOnEdit persistPropertyOnEdit;
  foreach (PropertyInfo propertyInfo in typeModelProperties)
  {
    persistPropertyOnEdit = 
      (PersistPropertyOnEdit)Attribute.GetCustomAttribute(
      typeModel.GetProperty(propertyInfo.Name), typeof(PersistPropertyOnEdit));
    if (persistPropertyOnEdit == null)
    {
    OutputList.Add(ModelVariable + "Entry.Property(e => e." +
      propertyInfo.Name + ").IsModified = true;");
    }
    else
    {
    OutputList.Add(ModelVariable + "Entry.Property(e => e." +
      propertyInfo.Name + ").IsModified = " +
      ((PersistPropertyOnEdit)persistPropertyOnEdit).
      PersistPostbackDataFlag.ToString().ToLower() + ";");
    }
  }
  return OutputList;
}

変更後のコントローラー テンプレートは、修正された Edit ポストバック アクション メソッドを生成します (図 12 参照)。GetPropertyIsModifiedList 関数は、このソース コードの一部を生成します。

図 12 新しくスキャフォールディングされたコントローラーの Edit ハンドラー

if (ModelState.IsValid)
{
  if (product is IControllerHooks) 
  {
   ((IControllerHooks)product).OnEdit(); 
  }
  db.Products.Attach(product);
  var productEntry = db.Entry(product);
  productEntry.Property(e => e.ProductId).IsModified = true;
  productEntry.Property(e => e.Description).IsModified = true;
  productEntry.Property(e => e.CreatedDate).IsModified = false;
  productEntry.Property(e => e.ModifiedDate).IsModified = true;
  db.SaveChanges();
  return RedirectToAction("Index");
}

CRUD ビューにプロパティが表示されないようにするために属性を使用する

ASP.NET MVC には、スキャフォールディングされたビューにモデルのプロパティをレンダリングするかどうかを制御する属性が 3 つしかありません (図 A 参照)。1 つ目と 2 つ目の属性 [Editable(false)] と [ReadOnly(true)] は、異なる名前空間に属していますが実行する内容は同じです (修飾したプロパティが、作成ビューと編集ビューにレンダリングされなくなります)。3 つ目の [ScaffoldColumn(false)] 属性を使用すると、修飾したプロパティが、レンダリングされるビューのいずれにも表示されなくなります。

図 A プロパティのレンダリングを行わないようにする 3 つの属性

モデル メタデータ属性 属性の名前空間 影響を受けるビュー 結果
  なし なし 通常の結果だけを提供する追加の属性はありません。

[Editable(false)]

[ReadOnly(true)]

Editable:

System.ComponentModel.DataAnnotations

ReadOnly:

System.ComponentModel

作成

編集

修飾したモデル プロパティがレンダリングされなくなります。
[ScaffoldColumn(false)] System.ComponentModel.DataAnnotations

作成

削除

詳細

編集

インデックス

修飾したモデル プロパティがレンダリングされなくなります。

ビューをカスタマイズする

編集ビューに、ユーザーが編集するのは避けたい値を表示することもあります。ASP.NET MVC の属性ではこの設定がサポートされません。今回は、編集ビューで ModifiedDate を表示するつもりですが、ユーザーに編集可能なフィールドであると思われないようにします。これを実装するには、以下のように、CustomAttributes.cs クラス モジュールで、DisplayOnEditView というカスタム属性を作成します。

public class DisplayOnEditView : Attribute
{
  public readonly bool DisplayFlag;               
  public DisplayOnEditView(bool displayFlag)
  {
    this.DisplayFlag = displayFlag;
  }
}

これにより、モデル プロパティを修飾し、編集ビューのラベルとしてレンダリングされるようにできます。その後、だれかがポストバック中に値を改ざんするという心配なく、編集ビューに ModifiedDate を表示できるようになります。

このカスタム属性は、Product モデルをさらに修飾するために使用することができます。今回はこの属性を、ModifiedDate プロパティに配置します。以下のように、作成ビューに表示されないよう [Editable(false)] を使用し、編集ビューのラベルとして表示されるよう [DisplayOnEditView(true)] を使用します。

public class Product : IControllerHooks
{
  public int ProductId { get; set; }
  public string Description { get; set; }
  [PersistPropertyOnEdit(false)]
  [ScaffoldColumn(false)]
  public DateTime? CreatedDate { get; set; }
  [Editable(false)]
  [DisplayOnEditView(true)]
  public DateTime? ModifiedDate { get; set; }
}

最後に、DisplayOnEditView 属性を使用可能にするため、編集ビューを生成する T4 テンプレートを以下のように変更します。

HtmlForDisplayOnEditViewAttribute =
  JW_ScaffoldEnhancement.Models.ScaffoldFunctions.
  GetHtmlForDisplayOnEditViewAttribute(
  ViewDataTypeName, property.PropertyName,
  property.IsReadOnly);

さらに、ScaffoldFunctions クラスに、GetHtmlForDisplayOnEditViewAttribute 関数を追加します (図 13 参照)。

GetHtmlForDisplayOnEditViewAttribute 関数は、属性が false の場合 Html.EditorFor を、属性が true の場合 Html.DisplayTextFor を返します。編集ビューは、ModifiedDate をラベルとして表示し、他のすべての非キー フィールドを編集可能なテキスト ボックスとして表示します (図 14 参照)。

図 13 カスタムの属性 DisplayOnEditViewFlag をサポートする、ScaffoldFunctions.GetHtmlForDisplayOnEditViewAttribute 静的関数

static public string GetHtmlForDisplayOnEditViewAttribute(
  string ViewDataTypeName, string PropertyName, bool IsReadOnly)
{
  string returnValue = String.Empty;
  Attribute displayOnEditView = null;
  Type typeModel = Type.GetType(ViewDataTypeName);
  if (typeModel != null)
  {
    displayOnEditView =
    (DisplayOnEditView)Attribute.GetCustomAttribute(typeModel.GetProperty(
    PropertyName), typeof(DisplayOnEditView));
    if (displayOnEditView == null)
    {
      if (IsReadOnly)
      { returnValue = String.Empty; }
      else
      { returnValue = "@Html.EditorFor(model => model." + 
          PropertyName + ")"; }
    }
    else
    {                         
      if (((DisplayOnEditView)displayOnEditView).DisplayFlag == true)
      { returnValue = "@Html.DisplayTextFor(model => model." + 
          PropertyName + ")"; }
      else
      { returnValue = "@Html.EditorFor(model => model." + 
        PropertyName + ")"; }
    }
  }
  return returnValue;
}

図 14 読み取り専用のフィールド ModifiedDate を表示する編集ビュー

まとめ

今回は、スキャフォールディング サブシステムで実行できることのほんの一部を説明しました。Entity Framework の CRUD コントロールやビューを提供するスキャフォールディングについて扱いましたが、別の種類の Web ページや Web API アクションのコードを生成できるスキャフォールディングも存在します。

T4 テンプレートを初めて使用される方は、まず既存のテンプレートをカスタマイズしてみることをお勧めします。ここで扱ったテンプレートは Visual Studio IDE のメニューから実行していますが、カスタム T4 テンプレートを作成して、必要に応じて変換することもできます。マイクロソフトのサイト (msdn.microsoft.com/ja-jp/vstudio/cc308634.aspx、英語) で、概要がわかりやすく説明されています。より高度な情報をお求めの方には、Dustin Davis のコースがお勧めです (bit.ly/1bNiVXU、英語)。

現時点では、Visual Studio 2013 には堅牢な T4 エディターが搭載されておらず、構文の強調表示機能や IntelliSense が提供されません。さいわい、そのためのアドオンがいくつかあります。Devart T4 Editor (bit.ly/1cabzOE、英語) や、tangible T4 Editor (bit.ly/1fswFbo、英語) をご利用ください。

Jonathan Waldman は、ベテランのソフトウェア開発者兼テクニカル アーキテクトで、マイクロソフトのテクノロジに初期から携わっています。有名なエンタープライズ プロジェクトで活動した経験や、ソフトウェア開発ライフサイクルのすべての側面に携わった経験があります。Pluralsight 技術チームのメンバーでもあり、ソフトウェア ソリューションや教材の開発を行っています。連絡先は、jonathan.waldman@live.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Joost de Nijs に心より感謝いたします。
Joost de Nijs は、マイクロソフトで Azure Developer Experience チームのプログラム マネージャーを務めていて、Web 開発者ツールの作成を行っています。現在は、Web スキャフォールディングや NuGet パッケージ管理機能の分野に重点的に取り組んでいて、以前は、Microsoft Azure Java クライアント ライブラリや Microsoft Azure デベロッパー センターで活動していました。