1

1つのページで更新するデータとオブジェクトのリスト(詳細)を含むオブジェクト(ヘッダー)を含むMVC3ページがあります。詳細オブジェクトには、実行する必要のあるカスタム検証(IValidatableObject)があります。

これは通常、期待どおりに機能しているように見えます。検証が実行され、ValidationResultsが返されます。また、@ Html.ValidationSummary(false); ページにそれらの検証が表示されます。ただし、検証のリストを上部に表示するのではなく、検証対象のアイテムの横に配置する必要があります。つまり、ページ上にあるが関連するメッセージを表示しないHtml.ValidationMessageForです。足りないものはありますか?これは他のページ(このマスター/詳細の状況がない)で機能しているので、更新するアイテムのリストまたはアイテムのエディターテンプレートを設定する方法についての何かだと思います?

Edit.cshtml(ヘッダー-詳細編集ビュー)

@foreach (var d in Model.Details.OrderBy(d => d.DetailId))
{
   @Html.EditorFor(item => d, "Detail")
}

Detail.ascx(詳細エディターテンプレート)

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Detail>" %>

<tr>            
    <td>
        <%= Model.Name %>
        <%= Html.HiddenFor(model => model.DetailId) %>
    </td>
    <td class="colDescription">
        <%= Html.EditorFor(model => model.Description) %>
        <%= Html.ValidationMessageFor(model => model.Description) %>
    </td>
    <td class="colAmount">
        <%= Html.EditorFor(model => model.Amount) %>
        <%= Html.ValidationMessageFor(model => model.Amount) %>
    </td>
</tr>

モデルは、NameとHeaderIdを持つヘッダーを持つエンティティフレームワークであり、DetailにはDetailId、HeaderId、Description、Amountがあります

コントローラーコード:

public ActionResult Edit(Header header, FormCollection formCollection)
{
   if (formCollection["saveButton"] != null)
   {
      header = this.ProcessFormCollectionHeader(header, formCollection);
      if (ModelState.IsValid)
      {
         return new RedirectResult("~/saveNotification");
      }
      else
      {
         return View("Edit", header);
      }
   }
   else
   {
      return View("Edit", header);
   }
}

[ここで何が起こっているのかを判断しようとした結果、この状態でコントローラーコードを少しクリーンアップできることはわかっています]

IValidatableObjectの実装:

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
   if (this.Name.Length < 5) && (this.Amount > 10))
   {
      yield return new ValidationResult("Item must have sensible name to have Amount larger than 10.", new[] { "Amount" });
   }
}
4

1 に答える 1

3

実際のエディターテンプレートを使用することをお勧めします。コードの問題は、ビュー内にforeachループを記述して、対応する入力フィールドに間違った名前を生成するテンプレートをレンダリングしていることです。header = this.ProcessFormCollectionHeader(header, formCollection);これが、単にモデルバインダーを使用してジョブを実行するのではなく、コントローラーアクションでいくつかの回避策を実行してモデル()にデータを入力する理由だと思います。

それを達成するための正しい方法をお見せしましょう。

モデル:

public class Header
{
    public IEnumerable<Detail> Details { get; set; }
}

public class Detail : IValidatableObject
{
    public int DetailId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public int Amount { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if ((this.Name ?? string.Empty).Length < 5 && this.Amount > 10)
        {
            yield return new ValidationResult(
                "Item must have sensible name to have Amount larger than 10.", 
                new[] { "Amount" }
            );
        }
    }
}

コントローラ:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new Header
        {
            Details = Enumerable.Range(1, 5).Select(x => new Detail
            {
                DetailId = x,
                Name = "n" + x,
                Amount = 50
            }).OrderBy(d => d.DetailId)
        };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(Header model)
    {
        if (ModelState.IsValid)
        {
            return Redirect("~/saveNotification");
        }
        return View(model);
    }
}

ビュー(~/Views/Home/Index.cshtml):

@model Header

@using (Html.BeginForm())
{
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Description</th>
                <th>Amount</th>
            </tr>
        </thead>
        <tbody>
            @Html.EditorFor(x => x.Details)
        </tbody>
    </table>
    <button type="submit">OK</button>
}

~/Views/Shared/EditorTemplates/Detail.ascx詳細タイプ(または~/Views/Shared/EditorTemplates/Detail.cshtmlRazor)のエディターテンプレート:

<%@ Control 
    Language="C#" 
    Inherits="System.Web.Mvc.ViewUserControl<MvcApplication1.Controllers.Detail>" 
%>

<tr>            
    <td>
        <%= Html.DisplayFor(model => model.Name) %>
        <%= Html.HiddenFor(model => model.DetailId) %>
        <%= Html.HiddenFor(model => model.Name) %>
    </td>
    <td class="colDescription">
        <%= Html.EditorFor(model => model.Description) %>
        <%= Html.ValidationMessageFor(model => model.Description) %>
    </td>
    <td class="colAmount">
        <%= Html.EditorFor(model => model.Amount) %>
        <%= Html.ValidationMessageFor(model => model.Amount) %>
    </td>
</tr>

コードを改善するために私が行ったことがいくつかあります。

  • コントローラーレベルでDetailIdによるDetailsコレクションの順序付けを実行しました。表示用のビューモデルを準備するのはコントローラーの責任です。ビューはこの順序付けを行ってはなりません。ビューが行う必要があるのは、データを表示することだけです
  • 以前の改善のおかげで、エディターテンプレートのレンダリングに使用していたビューのforeachループを削除し、1回の@Html.EditorFor(x => x.Details)呼び出しに置き換えました。これが機能する方法は、ASP.NET MVCが(タイプの)Detailsコレクションプロパティであることを検出し、 or (コレクションのタイプと同じ名前)と呼ばれるまたはフォルダーIEnumerable<Detail>内にテンプレート化されたカスタムエディターを自動的に検索することです。次に、コレクションの要素ごとにこのテンプレートをレンダリングするので、心配する必要はありません。~/Views/SomeController/EditorTemplates~/Views/Shared/EditorTemplatesDetail.ascxDetail.cshtml
  • 以前の改善のおかげで、アクション内でハック[HttpPost]する必要がなくなりました。ProcessFormCollectionHeaderアクション引数はheader、モデルバインダーによってリクエストデータから正しくバインドされます
  • Detail.ascxテンプレート内で、出力を適切にHTMLエンコードし、サイトで開いていたXSSの穴を埋めるために置き換え<%= Model.Name %>ました。<%= Html.DisplayFor(model => model.Name) %>
  • Validateメソッド内で、プロパティのName長さをテストする前に、プロパティがnullでないことを確認しました。ちなみに、あなたの例では、テンプレート内に説明フィールドの入力フィールドしかなく、Nameプロパティに対応する入力フィールドがなかったため、フォームが送信されると、このプロパティは常にnullになります。結果として、対応する非表示の入力フィールドを追加しました。
于 2012-10-09T07:29:00.200 に答える