Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
122 views
in Technique[技术] by (71.8m points)

c# - Add item into List from View and pass to Controller in MVC5

I have a form like this: enter image description here

JS Code:

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

Old Code:

Model:

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

Controller:

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

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

I choose a color and click Add, it'll add more item in to list. I'm newbie in ASP.NET MVC. I just know value from Razor how to pass value form

I also asked about same thing in here and received kind explain. But, it's static value which pass from controller and then is used to bind into razor. But now, it isn't static.

Could you tell me how to bind razor item in to list to post it to controller? I would be very grateful if you give me some suggest.

Thanks for your kind helping. (bow)

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You can refer this post. It works perfect for me.

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

I will quote it below:

The aspects I will consider are:

  1. Dynamically adding, removing and reordering items to/from the collection
  2. Validation implications
  3. Code Reusability and Refactoring implications I will assume that you are already familiar with ASP.NET MVC and basic JavaScript concepts.

Source Code All source code is available on GitHub

The Sample What I am going to build is a little sample where we have a user who has a list of favourite movies. It will look roughly like on the image below and will allow for adding new favourite movies, removing favourite movies and also reordering them up and down using the drag handler.

In Part 1 I look at implementing collection editing by sticking to facilities provided to us by ASP.NET MVC such as views, partial views, editor templates, model binding, model validation, etc.

Domain Model The domain model is basically:

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

and

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

Let’s get cracking!

An Edit View Let’s start by creating a first-pass edit view for our Person to look like the one on the image above:

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

he view is creating a list of editing controls for each of the movies in Person.FavouriteMovies. I am using a jQuery selector and dom function to remove a movie when the user clicks “Delete” and also a jQuery UI Sortable to make the items from the HTML list drag and droppable up and down.

With this done we immediately face the first problem: We haven’t implemented the “Add another”. Before we do that let’s consider how ASP.NET MVC model binding of collections works.

ASP.NET MVC Collection Model Binding Patterns There are two patterns for model binding collections in ASP.NET MVC. The first one you have just seen:

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

which generates similar 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>

This is really great for displaying collections and editing static length collections, but problematic when we want to edit variable length collections, because:

1. Indices have to be sequential (0, 1, 2, 3, …). If they aren’t ASP.NET MVC stops at the first gap. E.g. if you have item 0, 1, 3, 4 after the model binding has finished you will end up with a collection of two items only – 1 and 2 instead of four items. 2. If you were to reorder the list in the HTML ASP.NET MVC will apply the indices order not the fields order when doing model binding.

This basically means that add/remove/reorder scenarios are no go with this. It’s not impossible but it will be big big mess tracking add/remove/reorder actions and re-indexing all field attributes.

Now, someone might say – “Hey, why don’t you just implement a non-sequential collection model binder?” .

Yes, you can write the code for a non-sequential collection model binder. You will however face two major issues with that however. The first being that the IValueProvider doesn’t expose a way to iterate through all values in the BindingContext which you can workaround* by hardcoding the model binder to access the current HttpRequest Form values collection (meaning that if someone decides to submit the form via Json or query parameters your model binder won’t work) or I’ve seen one more insane workaround which checks the *BindingContext one by one from CollectionName[0] to CollectionName[Int32.MaxValue] (that’s 2 billion iterations!).

Second major issue is that once you create a sequential collection from the non-sequential indices and items and you have a validation error and you re-render the form view your ModelState will no longer match the data. An item that used to be at index X is now at index X-1 after another item before it was deleted, however the ModelState validation message and state still point to X, because this is what you submitted.

So, even a custom model binder won’t help.

Thankfully there is a second pattern, which mostly helps for what we want to achieve (even though I don’t think it was designed to solve exactly this):

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

Notice how we have introduced an “.Index” hidden field for each collection item. By doing that we tell ASP.NET MVC’s model binding “Hey, don’t look for a standard numeric collection index, but instead look for the custom Index value we have specified and just get me the list of items in a collection when you are done”. How does this help?

We can specify any index value we want The index doesn’t have to be sequential and items will be put in the collection in the order they are in the HTML when submitted. Bam! That’s solves most, but not all of our problems.

The Solution

Firstly, ASP.NET MVC doesn’t have HTML helpers to generate the “[something].Index” pattern which is major problem since it means we can’t use validation and custom editors. We can fix that by utilizing some ASP.NET templating fu. What we are going to do is move the Movie editor to a its own partial view (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>

And update our Edit view to use it:

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

Notice two things – firstly the Movie partial edit view uses standard Html helpers and secondly there is a call to something custom called Html.BeginCollectionItem. *You might even ask yourself: Wait a second. This won’t work, because the partial view will produce names such as “Title” instead of “FavouriteMovies[xxx].Title”, so let me show you the source code of *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.Html

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...