@Html.HiddenFor does not work on Lists in ASP.NET MVC
Asked Answered
C

14

118

I'm using a model that contains a List as a property. I'm populating this list with items i grab from SQL Server. I want the List to be hidden in the view and passed to the POST action. Later on i may want to add more items to this List with jQuery which makes an array unsuitable for expansion later on. Normally you would use

@Html.HiddenFor(model => model.MyList)

to accomplish this functionality, but for some reason the List in POST is always null.

Very simple question, anyone know why MVC behaves like this?

Cottontail answered 21/2, 2012 at 21:15 Comment(5)
Normally you wouldn't hide entire lists like that. What is your desired output in terms of <input />s?Wyattwyche
what does MyList contain? HiddenFor is only used for one input at a time.Acetometer
What Type is Model.MyList? You may need to perform some serialization/deserialization on your list manually.Bandmaster
[#4382371 Similar question.Descent
Similar Question: Use of HiddenFor with intellisenseDescent
K
197

I've just come across this issue and solved it simply by doing the following:

@for(int i = 0; i < Model.ToGroups.Count; i++)
{
    @Html.HiddenFor(model => Model.ToGroups[i])
}

By using a for instead of a foreach the model binding will work correctly and pick up all of your hidden values in the list. Seems like the simplest way to solve this problem.

Kentigerma answered 29/9, 2014 at 6:13 Comment(6)
Thanks - nice simple solution. Just a tiny mod needed though: The object's Id field needs to be referred to. So if the field is called RowId, then: @Html.HiddenFor(model => Model.ToGroups[i].RowId)Penrose
worked for me, even for when I had multiple fields on the models in the collection. I.e. @Html.EditorFor(model => Model.ToGroups[i].Id) followed by @Html.EditorFor(model => Model.ToGroups[i].Description) on the next time - both in the for-loop. And the controller was able to map it to a list of the models with those fields. And to make sure none of it showed up on screen, just surround it in <div style="display: none;"></div>Anana
I ran into issues with this and still haven't figured out a solution. I had something like @Html.DropdownListFor(m => m.Departments, new SelectList(Model.Lookup, "Code", "Name) and in my loop I had for(int i = 0; i < Model.Departments; i++) @Html.HiddenFor(model => Model.Departments[i]) but nothing was being passed in on POST. Am I doing something wrong?Algo
@Algo Answering a really old comment here, but maybe someone else will have the same issue: Change the for-loop to this: for(int i = 0; i < Model.Departments.Count(); i++)Dromond
Does anyone know if there is an alternative to this insidious hack that bloats all of the HTML? Why doesn't ASP.NET Core MVC just bind via the [Bind] format that's supplied in the post function? Surely there has to be a better way... ?Allistir
One very dummy reason is also that the properties were declared as fields, without { get; set; } ... It took me some time to figure this out in my caseSibylsibylla
G
29

HiddenFor is not like a DisplayFor or EditorFor. It won't work with collections, only single values.

You can use the Serialize HTML helper available in the MVC Futures project to serialize an object to a Hidden field, or you will have to write the code yourself. A better solution is to simply serialize an ID of some sort and re-get the data from the database on postback.

Grandee answered 21/2, 2012 at 21:24 Comment(4)
Do you have an example? I tried this and it failed to bind to the ViewModel value when the form was submitted.Incase
@AlanMacdonald - if something fails to bind, it's because your naming is not correct, more than likely because you used a foreach instead of a for with indexer. Or maybe you didn't use the proper attributes in the binding. See weblogs.asp.net/shijuvarghese/archive/2010/03/06/…Grandee
Thanks. Actually when I tried it is was literally @Html.Serialize("Model.ModelIDs", Model.ModelIDs) where Model was my ViewModel and it had a ModelIDs int array property. So there were no loops or anything. When the form was submitted the ModelIDs were always null in the bound ViewModel.Incase
@AlanMacdonald - You don't include "Model" in the name.Grandee
J
19

It's a bit of a hack, but if @Html.EditorFor or @Html.DisplayFor work for your list, if you want to make sure it's sent on the post request but not visible, you could just style it to using display: none; to hide it instead, e.g:

<div style="display: none;">@Html.EditorFor(model => model.MyList)</div>
Jan answered 15/1, 2014 at 15:32 Comment(2)
This does not save the value in the model on post of the request.Velarde
If .EditorFor is set up to work correctly, then this should work too I believe.Jan
L
14

What about using Newtonsoft to deserialize the object into a json string and then insert that into your Hidden field e.g. (Model.DataResponse.Entity.Commission is a List of simple "CommissionRange" objects as you'll see in the JSON)

@using (Ajax.BeginForm("Settings", "AffiliateProgram", Model.DataResponse, new AjaxOptions { UpdateTargetId = "result" }))
   {
      string commissionJson = JsonConvert.SerializeObject(Model.DataResponse.Entity.Commission);
      @Html.HiddenFor(data => data.DataResponse.Entity.Guid)
      @Html.Hidden("DataResponse_Entity_Commission", commissionJson)
      [Rest of my form]
   }

Renders as:

<input id="DataResponse_Entity_Commission" name="DataResponse_Entity_Commission" type="hidden" value="[{"RangeStart":0,"RangeEnd":0,"CommissionPercent":2.00000},{"RangeStart":1,"RangeEnd":2,"CommissionPercent":3.00000},{"RangeStart":2,"RangeEnd":0,"CommissionPercent":2.00000},{"RangeStart":3,"RangeEnd":2,"CommissionPercent":1.00000},{"RangeStart":15,"RangeEnd":10,"CommissionPercent":5.00000}]">

In my case I do some JS stuff to edit the json in the hidden field before posting back

In my controller I then use Newtonsoft again to deserialize:

string jsonCommissionRange = Request.Form["DataResponse_Entity_Commission"];
List<CommissionRange> commissionRange = JsonConvert.DeserializeObject<List<CommissionRange>>(jsonCommissionRange);
Lyle answered 25/11, 2015 at 22:37 Comment(1)
This worked for me. I thought it was much cleaner that the accepted solution.Crooks
B
6

Html.HiddenFor is designed for only one value. You will need to serialize your list in some way before creating the hidden field.

For example, if your list is of type string, you could join the list into a comma separated list, then split the list after post back in your controller.

Bandmaster answered 21/2, 2012 at 21:22 Comment(0)
S
4

I've just found out (after a couple of hours of trying to figure out why model values weren't going back to the controller) that hidden for should follow the EditorFor.

Unless I am doing something else wrong this is what I found. I will not make the mistake again.

In the context of a Model that contains a list of another class.

This will NOT work:

        @{
            for (int i = 0; i < Model.Categories.Count; i++)
            {
                <tr>
                    <td>
                        @Html.HiddenFor(modelItem => Model.Categories[i].Id)
                        @Html.HiddenFor(modelItem => Model.Categories[i].ProductCategoryId)
                        @Html.HiddenFor(modelItem => Model.Categories[i].CategoryName)                            
                        @Html.DisplayFor(modelItem => Model.Categories[i].CategoryName)                            
                    </td>
                    <td>
                        @Html.HiddenFor(modelItem => Model.Categories[i].DailyPurchaseLimit)                                                        
                        @Html.EditorFor(modelItem => Model.Categories[i].DailyPurchaseLimit)
                        @Html.ValidationMessageFor(modelItem => Model.Categories[i].DailyPurchaseLimit)
                    </td>
                    <td style="text-align: center">
                        @Html.HiddenFor(modelItem => Model.Categories[i].IsSelected)                            
                        @Html.EditorFor(modelItem => Model.Categories[i].IsSelected)
                    </td>
                </tr>
            }
        }

Where as this WILL......

            for (int i = 0; i < Model.Categories.Count; i++)
            {
                <tr>
                    <td>
                        @Html.HiddenFor(modelItem => Model.Categories[i].Id)
                        @Html.HiddenFor(modelItem => Model.Categories[i].ProductCategoryId)
                        @Html.HiddenFor(modelItem => Model.Categories[i].CategoryName)                            
                        @Html.DisplayFor(modelItem => Model.Categories[i].CategoryName)                            
                    </td>
                    <td>
                        @Html.EditorFor(modelItem => Model.Categories[i].DailyPurchaseLimit)
                        @Html.HiddenFor(modelItem => Model.Categories[i].DailyPurchaseLimit)                            
                        @Html.ValidationMessageFor(modelItem => Model.Categories[i].DailyPurchaseLimit)
                    </td>
                    <td style="text-align: center">
                        @Html.EditorFor(modelItem => Model.Categories[i].IsSelected)
                        @Html.HiddenFor(modelItem => Model.Categories[i].IsSelected)                            
                    </td>
                </tr>
            }
Semitrailer answered 9/10, 2015 at 12:43 Comment(0)
L
3

I started digging through the source code for HiddenFor, and I think the roadblock you're seeing is that your complex object MyList is not implicitly convertible to type string, so the framework treats your Model value as null and renders the value attribute empty.

Longs answered 21/2, 2012 at 21:52 Comment(0)
G
3

You can take a look on this solution.

Put only HiddenFor inside the EditorTemplate.

And in your View put this: @Html.EditorFor(model => model.MyList)

It should works.

Gerick answered 15/11, 2012 at 8:40 Comment(0)
H
3

Faced the same issue. Without for loop, it only posted the first element of the list. After iterating through for loop, it can keep full list and post successfully.

 @if (Model.MyList!= null)
    {
    for (int i = 0; i < Model.MyList.Count; i++)
      {
        @Html.HiddenFor(x => x.MyList[i])
      }
    }
Halstead answered 23/11, 2017 at 8:55 Comment(0)
W
2

Another option would be:

<input type="hidden" value=@(string.Join(",", Model.MyList)) />
Woodbury answered 16/3, 2018 at 19:30 Comment(1)
This was my first idea too. But I had a view model, that expected an int[] for the MyList field and comma separated string is not parsed into an array by the MVC binding mechanism.Nitpicking
G
2

The foreach loop instead of a for loop might be a slightly cleaner solution.

@foreach(var item in Model.ToGroups)
{
    @Html.HiddenFor(model => item)
}
Grissel answered 31/7, 2019 at 12:52 Comment(0)
P
1

Another possible way to fix this would be to give each object in your List an ID, then use @Html.DropDownListFor(model => model.IDs) and populate an array which holds the IDs.

Propagation answered 24/6, 2014 at 20:43 Comment(0)
D
1

maybe late, but i created extension method for hidden fields from collection (with simple data type items):

So here it is:

/// <summary>
/// Returns an HTML hidden input element for each item in the object's property (collection) that is represented by the specified expression.
/// </summary>
public static IHtmlString HiddenForCollection<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression) where TProperty : ICollection
{
    var model = html.ViewData.Model;
    var property = model != null
                ? expression.Compile().Invoke(model)
                : default(TProperty);

    var result = new StringBuilder();
    if (property != null && property.Count > 0)
    {
        for(int i = 0; i < property.Count; i++)
        {
            var modelExp = expression.Parameters.First();
            var propertyExp = expression.Body;
            var itemExp = Expression.ArrayIndex(propertyExp, Expression.Constant(i));

            var itemExpression = Expression.Lambda<Func<TModel, object>>(itemExp, modelExp);

            result.AppendLine(html.HiddenFor(itemExpression).ToString());
        }
    }

    return new MvcHtmlString(result.ToString());
}

Usage is as simple as:

@Html.HiddenForCollection(m => m.MyList)
Deandre answered 20/9, 2017 at 9:30 Comment(0)
N
1

Adding to this answer, I had a Model with various properties, some of which were IEnumerables. What I did was to serialize it in a variable in the view with:

@{
    var serializedObject = JsonSerializer.Serialize(Model);
}

And then, put that string into a Hidden element (since HiddenFor is intended for a single value from a Model) like this:

@Html.Hidden("serialized", @serializedObject)

And finally, in the controller, I could deserialize it with

JsonSerializer.Deserialize<MyType>(Request.Form["serialized"])
Noenoel answered 10/6, 2022 at 18:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.