web-dev-qa-db-fra.com

Ajouter un élément dans la liste depuis la vue et passer au contrôleur dans MVC5

J'ai un formulaire comme celui-ci: enter image description here

Code 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)
    });
});

Ancien code:

Modèle:

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; }
    }
}

Contrôleur:

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();
        }
    }

Affichage:

@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>
}

Je choisis une couleur et cliquez sur Ajouter, cela ajoutera plus d'éléments à la liste. Je suis novice en ASP.NET MVC. Je sais juste la valeur de Razor comment passer la forme de valeur

J'ai également posé des questions sur la même chose en ici et j'ai reçu une sorte d'explication. Mais, c'est la valeur statique qui passe du contrôleur et est ensuite utilisée pour se lier au rasoir. Mais maintenant, ce n'est pas statique.

Pourriez-vous me dire comment lier un élément de rasoir à une liste pour le publier sur le contrôleur? Je vous serais très reconnaissant de bien vouloir me donner quelques suggestions.

Merci pour votre aimable aide. (arc)

7
Long Nguyen

Vous pouvez référer ce post. Cela fonctionne parfaitement pour moi.

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

Je vais le citer ci-dessous:

Les aspects que je considérerai sont:

  1. Ajout, suppression et réorganisation dynamiques d'éléments de/vers la collection
  2. Implications de validation
  3. Réutilisation du code et implications de la refactorisation Je suppose que vous connaissez déjà ASP.NET MVC et les concepts de base de JavaScript.

Code source Tout le code source est disponible sur GitHub

L'échantillon Ce que je vais construire est un petit échantillon où nous avons un utilisateur qui a une liste de films préférés. Il ressemblera à peu près à l'image ci-dessous et permettra d'ajouter de nouveaux films préférés, de supprimer des films préférés et de les réorganiser de haut en bas à l'aide du gestionnaire de glisser.

Dans la partie 1, j'examine l'implémentation de l'édition de collection en respectant les fonctionnalités fournies par ASP.NET MVC telles que les vues, les vues partielles, les modèles d'éditeur, la liaison de modèle, la validation de modèle, etc.

Modèle de domaine Le modèle de domaine est essentiellement:

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

et

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

Allons craquer!

Une vue d'édition Commençons par créer une vue d'édition de premier passage pour que notre personne ressemble à celle de l'image ci-dessus:

@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>
}

la vue crée une liste de contrôles d'édition pour chacun des films dans Person.FavouriteMovies. J'utilise un sélecteur jQuery et une fonction dom pour supprimer un film lorsque l'utilisateur clique sur "Supprimer" et également un jQuery UI Sortable pour rendre les éléments de la liste HTML glisser et largables de haut en bas.

Avec cela fait, nous sommes immédiatement confrontés au premier problème: nous n'avons pas mis en œuvre le "Ajouter un autre". Avant de le faire, examinons le fonctionnement de la liaison de modèles ASP.NET MVC des collections.

Modèles de liaison de modèle de collection ASP.NET MVC Il existe deux modèles pour les collections de liaison de modèle dans ASP.NET MVC. Le premier que vous venez de voir:

@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)
…
}

qui génère un HTML similaire:

<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>

C'est vraiment génial pour afficher des collections et modifier des collections de longueur statique, mais problématique lorsque nous voulons modifier des collections de longueur variable, car:

1. Les indices doivent être séquentiels (0, 1, 2, 3,…). S'ils ne sont pas ASP.NET MVC s'arrête au premier écart. Par exemple. si vous avez l'article 0, 1, 3, 4 après la fin de la reliure du modèle, vous vous retrouverez avec une collection de deux articles seulement - 1 et 2 au lieu de quatre articles. 2. Si vous deviez réorganiser la liste dans HTML ASP.NET, MVC appliquera l'ordre des indices et non l'ordre des champs lors de la liaison du modèle.

Cela signifie essentiellement que les scénarios d'ajout/suppression/réorganisation ne sont pas compatibles avec cela. Ce n'est pas impossible, mais ce sera un gros gros désordre qui suivra les actions d'ajout/suppression/réorganisation et la réindexation de tous les attributs de champ.

Maintenant, quelqu'un pourrait dire - "Hé, pourquoi ne pas simplement implémenter un classeur de modèle de collection non séquentiel?".

Oui, vous pouvez écrire le code d'un classeur de modèle de collection non séquentiel. Vous devrez cependant faire face à deux problèmes majeurs avec cela. La première étant que l'IValueProvider n'expose pas un moyen d'itérer à travers toutes les valeurs dans le BindingContext que vous pouvez contourner * en codant en dur le classeur de modèle pour accéder à la collection actuelle de valeurs du formulaire HttpRequest Form (ce qui signifie que si quelqu'un décide de soumettre le formulaire via Json ou les paramètres de requête que votre classeur de modèle ne fonctionnera pas) ou j'ai vu une autre solution insensée qui vérifie le * BindingContext un par un de CollectionName [0] à CollectionName [Int32.MaxValue] (c'est 2 milliards d'itérations!).

Le deuxième problème majeur est qu'une fois que vous créez une collection séquentielle à partir des index et des éléments non séquentiels et que vous avez une erreur de validation et que vous restituez la vue du formulaire, votre ModelState ne correspondra plus aux données. Un élément qui était à l'index X est maintenant à l'index X-1 après un autre élément avant d'être supprimé, mais le message de validation et l'état de ModelState pointent toujours vers X, car c'est ce que vous avez soumis.

Ainsi, même un classeur de modèle personnalisé n’aidera pas.

Heureusement, il existe un deuxième modèle, qui aide principalement à ce que nous voulons réaliser (même si je ne pense pas qu'il a été conçu pour résoudre exactement cela):

<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="" />

Remarquez comment nous avons introduit un champ caché ".Index" pour chaque élément de collection. En faisant cela, nous disons à la liaison de modèle d'ASP.NET MVC "Hé, ne cherchez pas un index de collection numérique standard, mais recherchez plutôt la valeur d'index personnalisée que nous avons spécifiée et obtenez-moi simplement la liste des éléments d'une collection lorsque vous êtes terminé". Comment cela aide-t-il?

Nous pouvons spécifier n'importe quelle valeur d'index que nous voulons L'index n'a pas besoin d'être séquentiel et les éléments seront placés dans la collection dans l'ordre où ils sont dans le HTML lors de leur soumission. Bam! Cela résout la plupart de nos problèmes, mais pas tous.

La solution

Premièrement, ASP.NET MVC ne dispose pas d’aides HTML pour générer le modèle "[quelque chose] .Index", ce qui est un problème majeur car cela signifie que nous ne pouvons pas utiliser la validation et les éditeurs personnalisés. Nous pouvons résoudre ce problème en utilisant certains fu de modèles ASP.NET. Ce que nous allons faire, c'est déplacer l'éditeur de film vers sa propre vue partielle (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>

Et mettez à jour notre vue Édition pour l'utiliser:

<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>

Remarquez deux choses: premièrement, la vue d'édition partielle de Movie utilise des assistants Html standard et deuxièmement il y a un appel à quelque chose de personnalisé appelé Html.BeginCollectionItem. * Vous pourriez même vous demander: attendez une seconde. Cela ne fonctionnera pas, car la vue partielle produira des noms tels que "Titre" au lieu de "FavouriteMovies [xxx] .Title", alors laissez-moi vous montrer le code source de * 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;
    }
}

Cet assistant fait deux choses:

  • Ajoute un champ d'index masqué à la sortie avec une valeur aléatoire GUID (rappelez-vous qu'en utilisant le modèle .Index, un index peut être n'importe quelle chaîne)
  • Étend l'exécution de l'assistant via un IDisposable et définit le contexte de rendu du modèle (assistants html et modèles d'affichage/éditeur) sur "FavouriteMovies [GUID]". Nous nous retrouvons donc avec du HTML comme ceci:

    Titre

Cela résout le problème de l'utilisation de modèles de champs Html et de la réutilisation des fonctionnalités ASP.NET au lieu d'avoir à écrire du html à la main, mais cela m'amène à la deuxième bizarrerie que nous devons résoudre.

Permettez-moi de vous montrer le deuxième et dernier problème. Désactivez la validation côté client et supprimez le titre, par ex. "Film 2" et cliquez sur soumettre. La validation échouera, car le titre d'un film est un champ obligatoire, mais alors que le formulaire d'édition s'affiche à nouveau ** il n'y a pas de message de validation **:

Pourquoi donc? C’est le même problème que j’ai mentionné plus tôt dans cet article. Chaque fois que nous rendons la vue, nous attribuons des noms différents aux champs, qui ne correspondent pas à ceux soumis et conduisent à une incohérence * ModelState *. Nous devons trouver comment conserver le nom et plus précisément l'index entre les demandes. Nous avons deux options:

Ajoutez un champ CollectionIndex et une propriété CollectionIndex masqués sur l'objet Movie pour conserver le FavouriteMovies.Index. Ceci est cependant intrusif et sous-optimal. Au lieu de polluer l'objet Movie avec une propriété supplémentaire, soyez intelligent et dans notre aide Html.BeginCollectionItem réappliquez/réutilisez les valeurs de formulaire FavouriteMovies.Index soumises. Remplaçons dans Html.BeginCollectionItem cette ligne:

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

avec:

string itemIndex = GetCollectionItemIndex(collectionIndexFieldName);

Et voici le code de 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();
}

Nous obtenons toutes les valeurs soumises, par exemple "FavouriteMovie.Index" les a placés dans une file d'attente que nous stockons pendant la durée de la demande. Chaque fois que nous rendons un élément de collection, nous retirons son ancienne valeur d'index et si aucun n'est disponible, nous en générons un nouveau. De cette façon, nous préservons l'index entre les demandes et pouvons avoir un ModelState cohérent et voir les erreurs de validation et les messages:

Il ne reste plus qu'à implémenter la fonctionnalité du bouton "Ajouter un autre" et nous pouvons le faire facilement en ajoutant une nouvelle ligne à l'éditeur de film, que nous pouvons récupérer en utilisant Ajax et utiliser notre vue partielle MovieEntryEditor.cshtml existante comme ça:

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

Et puis ajoutez le gestionnaire de clic suivant "Ajouter un autre":

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

Terminé;

Conclusion Bien qu'il ne soit pas immédiatement évident d'éditer des collections réorganisables de longueur variable avec ASP.NET MVC standard est possible et ce que j'aime dans cette approche est que:

Nous pouvons continuer à utiliser les assistants html ASP.NET traditionnels, l'éditeur et les modèles d'affichage (Html.EditorFor, etc.) avec dans notre édition de collection Nous pouvons utiliser le côté client et serveur de validation du modèle ASP.NET MVC Ce que je n'aime pas c'est pourtant beaucoup:

Que nous devons utiliser une demande AJAX pour ajouter une nouvelle ligne à l'éditeur. Que nous devons utiliser le nom de la collection dans la vue partielle de l'éditeur de film, mais sinon lors de l'exécution autonome = AJAX get request le contexte du nom ne sera pas correctement défini pour les champs de modèle partiels. J'adorerais avoir votre avis. L'exemple de code source est disponible sur mon GitHub


Autre moyen: http://blog.stevensanderson.com/2008/12/22/editing-a-variable-length-list-of-items-in-aspnet-mvc/

23
Long Nguyen