web-dev-qa-db-ja.com

ビューからリストにアイテムを追加し、MVC5のコントローラーに渡します

次のようなフォームがあります: enter image description here

JSコード:

$(document).ready(function() {
    $("#add-more").click(function() {
        selectedColor = $("#select-color option:selected").val();
        if (selectedColor == '')
            return;
        var color = ' <
            div class = "form-group" >
            <
            label class = "col-md-2 control-label" > Color: < /label> <
            div class = "col-md-5" > < label class = "control-label" > ' + selectedColor + ' < /label></div >
            <
            /div>
        ';
        var sizeAndQuantity = ' <
            div class = "form-group" >
            <
            label class = "col-md-2 control-label" > Size and Quantity: < /label> <
            div class = "col-md-2" > < label class = "control-label" > S < /label><input type="text" class="form-control"></div >
            <
            div class = "col-md-2" > < label class = "control-label" > M < /label><input type="text" class="form-control"></div >
            <
            div class = "col-md-2" > < label class = "control-label" > L < /label><input type="text" class="form-control"></div >
            <
            div class = "col-md-2" > < label class = "control-label" > XL < /label><input type="text" class="form-control"></div >
            <
            /div>
        ';
        html = color + sizeAndQuantity
        $("#appendTarget").append(html)
    });
});

古いコード:

モデル:

namespace ProjectSem3.Areas.Admin.Models
{
    public class ProductViewModel
    {
        public ProductGeneral ProductGeneral { get; set; }
        public List<SizeColorQuantityViewModel> SizeColorQuantities { get; set; }
    }
    public class ProductGeneral
    {
        public string Product { get; set; }
        public string Description { get; set; }
        public string ShortDescription { get; set; }
        public List<ProductCategory> Categories { get; set; }
        public string SKU { get; set; }
        public float Price { get; set; }
        public float PromotionPrice { get; set; }
        public bool Status { get; set; }
    }

    public class SizeColorQuantityViewModel
    {
        public string ColorId { get; set; }
        public List<SizeAndQuantity> SizeAndQuantities { get; set; }
    }
    public class SizeAndQuantity
    {
        public string SizeId { get; set; }
        public int Quantity { get; set; }
    }
}

コントローラー:

public class ProductController : Controller
    {
        // GET: Admin/Product
        public ActionResult Create()
        {
            var colors = new List<string>() { "Red", "Blue" };
            var sizes = new List<string>() { "S", "M", "L", "XL" };
            var categories = new ProductDao().LoadProductCategory();

            var productGeneral = new ProductGeneral()
            {
                Categories = categories
            };
            var model = new ProductViewModel
            {
                ProductGeneral = productGeneral,
                SizeColorQuantities = new List<SizeColorQuantityViewModel>()
            };


            foreach (var color in colors)
            {
                var child = new SizeColorQuantityViewModel
                {
                    ColorId = color,
                    SizeAndQuantities = new List<SizeAndQuantity>()
                };
                model.SizeColorQuantities.Add(child);
                foreach (var size in sizes)
                {
                    child.SizeAndQuantities.Add(new SizeAndQuantity()
                    {
                        SizeId = size 
                    });
                }
            }
            return View(model);
        }

        // POST: Admin/Product
        [HttpPost]
        public ActionResult Create(ProductViewModel model)
        {
            return View();
        }
    }

表示:

@for (var i = 0; i < Model.SizeColorQuantities.Count; i++)
{
<div class="form-group">
   <label class="col-md-2 control-label">Color:</label>
   <div class="col-md-2">
      @Html.TextBoxFor(m => m.SizeColorQuantities[i].ColorId, new { @class = "form-control", @readonly = "readonly" })
   </div>
</div>
<div class="form-group">
   <label class="col-md-2 control-label">Size and Quantity:</label>
   @for (var j = 0; j < Model.SizeColorQuantities[i].SizeAndQuantities.Count; j++)
   {
   <div class="col-md-2">
      @Html.TextBoxFor(m => m.SizeColorQuantities[i].SizeAndQuantities[j].SizeId, new
      {
      @class = "form-control",
      @style = "margin-bottom: 15px",
      @readonly = "readonly"
      })
      @Html.TextBoxFor(m => m.SizeColorQuantities[i].SizeAndQuantities[j].Quantity, new { @class = "form-control" })
   </div>
   }
</div>
}

色を選択して[追加]をクリックすると、リストに項目が追加されます。私はASP.NET MVCの初心者です。 Razorから値を取得する方法

私も here で同じことを尋ね、親切な説明を受けました。しかし、コントローラーから渡されてかみそりにバインドするために使用されるのは静的な値です。しかし、今では静的ではありません。

かみそりアイテムをリストにバインドしてコントローラーに投稿する方法を教えてください。あなたが私にいくつかの提案を与えれば私は非常に感謝します。

ご協力いただきありがとうございます。 (弓)

7
Long Nguyen

この投稿を参照できます。それは私にぴったりです。

http://ivanz.com/2011/06/16/editing-variable-length-reorderable-collections-in-asp-net-mvc-part-1/

以下に引用します。

私が検討する側面は次のとおりです:

  1. コレクションへのアイテムの動的な追加、削除、並べ替え
  2. 検証の意味
  3. コードの再利用性とリファクタリングの意味ASP.NET MVCとJavaScriptの基本的な概念をすでにご存じだと思います。

ソースコードすべてのソースコードはGitHubで利用可能

サンプル私が作成しようとしているのは、お気に入りの映画のリストを持っているユーザーがいる小さなサンプルです。下の画像のようになり、新しいお気に入りの映画を追加したり、お気に入りの映画を削除したり、ドラッグハンドラーを使用して上下に並べ替えたりすることができます。

パート1では、ビュー、部分ビュー、エディターテンプレート、モデルバインディング、モデル検証など、ASP.NET MVCから提供された機能に固執することでコレクション編集の実装を検討します。

ドメインモデルドメインモデルは基本的に次のとおりです。

public class User
{
    public int? Id { get; set; }
    [Required]
    public string Name { get; set; }
    public IList<Movie> FavouriteMovies { get; set; }
}

そして

public class Movie
{
    [Required]
    public string Title { get; set; }
    public int Rating { get; set; }
}

割れましょう!

編集ビューまず、上記の画像のように見えるように、Personの最初のパスの編集ビューを作成します。

@model CollectionEditing.Models.User
@{ ViewBag.Title = "Edit My Account"; }

<h2>Edit</h2>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>My Details</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>
    </fieldset>

    <fieldset>
        <legend>My Favourite Movies</legend>

        @if (Model.FavouriteMovies == null || Model.FavouriteMovies.Count == 0) {
            <p>None.</p>
        } else {
            <ul id="movieEditor" style="list-style-type: none">
                @for (int i=0; i < Model.FavouriteMovies.Count; i++) {
                    <li style="padding-bottom:15px">
                        <img src="@Url.Content("~/Content/images/draggable-icon.png")" style="cursor: move" alt=""/>

                        @Html.LabelFor(model => model.FavouriteMovies[i].Title)
                        @Html.EditorFor(model => model.FavouriteMovies[i].Title)
                        @Html.ValidationMessageFor(model => model.FavouriteMovies[i].Title)

                        @Html.LabelFor(model => model.FavouriteMovies[i].Rating)
                        @Html.EditorFor(model => model.FavouriteMovies[i].Rating)
                        @Html.ValidationMessageFor(model => model.FavouriteMovies[i].Rating)

                        <a href="#" onclick="$(this).parent().remove();">Delete</a>
                    </li>
                }
            </ul>
            <a href="#">Add another</a>
        }

        <script type="text/javascript">
            $(function () {
                $("#movieEditor").sortable();
            });
        </script>
    </fieldset>

    <p>
        <input type="submit" value="Save" />
        <a href="/">Cancel</a>
    </p>
}

ビューは、Person.FavouriteMoviesの各映画の編集コントロールのリストを作成しています。 jQueryセレクターとdom関数を使用して、ユーザーが[削除]をクリックしたときにムービーを削除します。また、HTMLリストのアイテムを上下にドラッグアンドドロップできるように jQuery UI Sortable を使用します。

これが完了すると、すぐに最初の問題に直面します。「追加」は実装していません。それを行う前に、コレクションのASP.NET MVCモデルバインディングの仕組みについて考えてみましょう。

ASP.NET MVCコレクションモデルバインディングパターンASP.NET MVCのモデルバインディングコレクションには2つのパターンがあります。あなたが最初に見たもの:

@for (int i=0; i < Model.FavouriteMovies.Count; i++) {
    @Html.LabelFor(model => model.FavouriteMovies[i].Title)
    @Html.EditorFor(model => model.FavouriteMovies[i].Title)
    @Html.ValidationMessageFor(model => model.FavouriteMovies[i].Title)
…
}

同様のHTMLを生成します:

<label for="FavouriteMovies_0__Title">Title</label>
<input id="FavouriteMovies_0__Title" name="FavouriteMovies[0].Title" type="text" value="" />
<span class="field-validation-error">The Title field is required.</span>

これは、コレクションの表示や静的な長さのコレクションの編集には非常に優れていますが、可変長のコレクションを編集する場合は問題があります。

1。インデックスは連続している必要があります(0、1、2、3、…)。 ASP.NET MVCでない場合、最初のギャップで停止します。例えば。モデルのバインドが完了した後にアイテム0、1、3、4がある場合は、4つのアイテムではなく1と2の2つのアイテムのみのコレクションになります。 2. HTMLでリストの順序を変更した場合、ASP.NET MVCはモデルのバインドを行うときに、フィールドの順序ではなくインデックスの順序を適用します。

これは基本的に、追加/削除/並べ替えのシナリオはこれに適さないことを意味します。不可能ではありませんが、追加/削除/並べ替えのアクションを追跡したり、すべてのフィールド属性のインデックスを再作成したりする大きな混乱になります。

今、誰かが言うかもしれない-「ねえ、なぜあなたは単に非シーケンシャルコレクションモデルバインダーを実装しないのですか?」 。

はい、非シーケンシャルコレクションモデルバインダーのコードを記述できます。ただし、これには2つの大きな問題があります。 1つ目は、IValueProviderがBindingContextのすべての値を反復処理する方法を公開していないことです。これは、モデルバインダーをハードコーディングして現在のHttpRequest Form値コレクションにアクセスすることで回避できます(つまり、誰かがJson経由でフォームを送信することを決定した場合または、モデルバインダーが機能しないクエリパラメーター)、または* BindingContextをCollectionName [0]からCollectionName [Int32.MaxValue]に1つずつ確認する非常に大きな回避策を見ました(これは20億回の繰り返しです!)。

2番目の主要な問題は、非順次インデックスと項目から順次コレクションを作成して検証エラーが発生し、ModelStateがデータと一致しなくなるというフォームビューの再レンダリングです。インデックスXにあったアイテムは、削除される前に別のアイテムの後にインデックスX-1にありますが、ModelState検証メッセージと状態はまだXを指します。これは送信したものだからです。

そのため、カスタムモデルのバインダーでも役に立ちません。

ありがたいことに、2番目のパターンがあります。これは、私たちが達成したいことのほとんどに役立ちます(正確にこれを解決するように設計されているとは思いませんが)。

<input type="hidden" name="FavouriteMovies.Index" value="indexA"/>
<input name="FavouriteMovies[indexA].Title" type="text" value="" />
<input name="FavouriteMovies[indexA].Rating" type="text" value="" />
<input type="hidden" name="FavouriteMovies.Index" value="indexB"/>
<input name="FavouriteMovies[indexB].Title" type="text" value="" />
<input name="FavouriteMovies[indexB].Rating" type="text" value="" />

各コレクション項目に「.Index」隠しフィールドを導入したことに注目してください。そうすることで、ASP.NET MVCのモデルバインディングに指示します。完了」。これはどのように役立ちますか?

必要なインデックス値を指定できます。インデックスはシーケンシャルである必要はなく、アイテムは送信時にHTML内の順序でコレクションに配置されます。バム!これでほとんど解決できますが、すべての問題が解決するわけではありません。

解決策

まず、ASP.NET MVCには「[something] .Index」パターンを生成するHTMLヘルパーがありません。これは、検証およびカスタムエディターを使用できないことを意味するため、大きな問題です。いくつかのASP.NETテンプレートfuを利用することで、これを修正できます。私たちがやろうとしていることは、ムービーエディターをそれ自身の部分ビュー(MovieEntryEditor.cshtml)に移動することです:

@model CollectionEditing.Models.Movie

<li style="padding-bottom:15px">
    @using (Html.BeginCollectionItem("FavouriteMovies")) {
        <img src="@Url.Content("~/Content/images/draggable-icon.png")" style="cursor: move" alt=""/>

        @Html.LabelFor(model => model.Title)
        @Html.EditorFor(model => model.Title)
        @Html.ValidationMessageFor(model => model.Title)

        @Html.LabelFor(model => model.Rating)
        @Html.EditorFor(model => model.Rating)
        @Html.ValidationMessageFor(model => model.Rating)

        <a href="#" onclick="$(this).parent().remove();">Delete</a>
    }
</li>

編集ビューを更新して使用します:

<ul id="movieEditor" style="list-style-type: none">
    @foreach (Movie movie in Model.FavouriteMovies) {
        Html.RenderPartial("MovieEntryEditor", movie);
    }
</ul>
<p><a id="addAnother" href="#">Add another</a>

2つの点に注意してください。まず、ムービーの部分編集ビューは標準のHtmlヘルパーを使用し、次にHtml.BeginCollectionItemと呼ばれるカスタム呼び出しがあります。 *あなたも自問するかもしれません:ちょっと待ってください。部分ビューでは「FavouriteMovies [xxx] .Title」ではなく「Title」などの名前が生成されるため、これは機能しません。したがって、* Html.BeginCollectionItemのソースコードを示します。

public static IDisposable BeginCollectionItem<TModel>(this HtmlHelper<TModel> html,                                                       string collectionName)
{
    string itemIndex = Guid.NewGuid().ToString();
    string collectionItemName = String.Format("{0}[{1}]", collectionName, itemIndex);

    TagBuilder indexField = new TagBuilder("input");
    indexField.MergeAttributes(new Dictionary<string, string>() {
        { "name", String.Format("{0}.Index", collectionName) },
        { "value", itemIndex },
        { "type", "hidden" },
        { "autocomplete", "off" }
    });

    html.ViewContext.Writer.WriteLine(indexField.ToString(TagRenderMode.SelfClosing));
    return new CollectionItemNamePrefixScope(html.ViewData.TemplateInfo, collectionItemName);
}

private class CollectionItemNamePrefixScope : IDisposable
{
    private readonly TemplateInfo _templateInfo;
    private readonly string _previousPrefix;

    public CollectionItemNamePrefixScope(TemplateInfo templateInfo, string collectionItemName)
    {
        this._templateInfo = templateInfo;

        _previousPrefix = templateInfo.HtmlFieldPrefix;
        templateInfo.HtmlFieldPrefix = collectionItemName;
    }

    public void Dispose()
    {
        _templateInfo.HtmlFieldPrefix = _previousPrefix;
    }
}

このヘルパーは2つのことを行います。

  • ランダムなGUID=値を使用して、非表示のIndexフィールドを出力に追加します(.Indexパターンを使用すると、インデックスには任意の文字列を使用できます)
  • IDisposableを介してヘルパーの実行をスコープし、テンプレートレンダリングコンテキスト(htmlヘルパーとディスプレイ/エディターテンプレート)を「FavouriteMovies [GUID]」に設定します。したがって、次のようなHTMLになります。

    題名

これにより、Htmlフィールドテンプレートを使用し、手作業でhtmlを記述する代わりに基本的にASP.NET機能を再利用する問題を解決できますが、対処する必要がある2番目の癖につながります。

最後の2番目の問題をお見せしましょう。クライアント側の検証を無効にし、たとえば「映画2」をクリックし、[送信]をクリックします。映画のタイトルは必須フィールドであるため、検証は失敗しますが、もう一度編集フォームが表示されている間**検証メッセージはありません**:

何故ですか?これは、この投稿で前述したのと同じ問題です。ビューをレンダリングするたびに、フィールドに異なる名前を割り当てます。これは、送信された名前とは一致せず、* ModelState * inconsistencyになります。名前、より具体的にはリクエスト間でインデックスを永続化する方法を理解する必要があります。次の2つのオプションがあります。

Movieオブジェクトに非表示のCollectionIndexフィールドとCollectionIndexプロパティを追加して、FavouriteMovies.Indexを永続化します。ただし、これは侵入的で最適ではありません。追加のプロパティを使用してMovieオブジェクトを汚染する代わりに、ヘルパーHtml.BeginCollectionItemで送信されたFavouriteMovies.Indexフォーム値を再適用/再利用します。 Html.BeginCollectionItemで次の行を置き換えましょう。

string itemIndex = Guid.New().ToString();

で:

string itemIndex = GetCollectionItemIndex(collectionIndexFieldName);

そして、GetCollectionItemIndexのコードは次のとおりです。

private static string GetCollectionItemIndex(string collectionIndexFieldName)
{
    Queue<string> previousIndices = (Queue<string>) HttpContext.Current.Items[collectionIndexFieldName];
    if (previousIndices == null) {
        HttpContext.Current.Items[collectionIndexFieldName] = previousIndices = new Queue<string>();

        string previousIndicesValues = HttpContext.Current.Request[collectionIndexFieldName];
        if (!String.IsNullOrWhiteSpace(previousIndicesValues)) {
            foreach (string index in previousIndicesValues.Split(','))
                previousIndices.Enqueue(index);
        }
    }

    return previousIndices.Count > 0 ? previousIndices.Dequeue() : Guid.NewGuid().ToString();
}

たとえば、送信されたすべての値を取得します。 「FavouriteMovie.Index」はそれらをキューに入れ、リクエストの期間中保存します。コレクションアイテムをレンダリングするたびに、古いインデックス値をデキューし、使用可能なインデックス値がない場合は、新しいインデックス値を生成します。これにより、リクエスト間でインデックスを保持し、一貫したModelStateを保持して検証エラーとメッセージを確認できます。

残っているのは、「別の追加」ボタン機能を実装することだけです。新しい行をムービーエディタに追加することで簡単に実行できます。

public ActionResult MovieEntryRow()
{
    return PartialView("MovieEntryEditor");
}

そして、次の「Add Another」クリックハンドラーを追加します。

$("#addAnother").click(function () {
    $.get('/User/MovieEntryRow', function (template) {
        $("#movieEditor").append(template);
    });
});

完了;

結論標準のASP.NET MVCを使用して可変長の並べ替え可能なコレクションを編集することはすぐには明らかではありませんが、このアプローチについて私が気に入っていることは次のとおりです。

コレクションの編集では、従来のASP.NET htmlヘルパー、エディターおよび表示テンプレート(Html.EditorForなど)を使用し続けることができます。ASP.NETMVCモデル検証クライアントおよびサーバー側を利用できます。ただし、それだけです。

AJAXリクエストをエディターに追加する必要があります。ムービーエディターのパーシャルビューでコレクションの名前を使用する必要がありますが、それ以外の場合はスタンドアロン= AJAX get request部分テンプレートフィールドに対して名前コンテキストが適切に設定されません。ご意見をお聞かせください。サンプルソースコードはGitHubで入手できます。


その他の方法: http://blog.stevensanderson.com/2008/12/22/editing-a-variable-length-list-of-items-in-aspnet-mvc/

23
Long Nguyen