John
John

Reputation: 2202

ASP.NET MVC Post list becomes null under very strange circumstances

So I have a controller like this:

public class TestController : Controller
    {
        //
        // GET: /Test/

        public ActionResult Index()
        {
            return View("Test");
        }

        public ActionResult Post(IList<Test> LanguageStrings, IList<Test> LanguageStringsGroup, IList<string> Deleted, IList<string> DeletedGroup)
        {
            if (LanguageStrings == null)
            {
                throw new ApplicationException("NULL");
            }


            return View("Test");
        }

    }

    public class Test
    {
        public string Val { get; set; }
        public string Another { get; set; }
    }

And a view like this:

<h2>Test</h2>

@using (Html.BeginForm("Post", "Test"))
{
    @Html.Hidden("LanguageStrings[0].Val", "test1")
    @Html.Hidden("LanguageStrings[0].Another")
    @Html.Hidden("LanguageStrings[1].Val", "test2")
    @Html.Hidden("LanguageStrings[1].Another")

    @Html.Hidden("LanguageStringsGroup[0].Val", "test4")

    @Html.Hidden("Deleted[0]")
    @Html.Hidden("Deleted[1]")
    @Html.Hidden("Deleted[2]")

    @Html.Hidden("DeletedGroup[0]")

    <button>Post</button>
}

When I post the form my controller throws the exception because LanguageStrings is null. The strange part I mentioned in the title is that if I add one more record to the list everything works. Like this:

<h2>Test</h2>

@using (Html.BeginForm("Post", "Test"))
{
    @Html.Hidden("LanguageStrings[0].Val", "test1")
    @Html.Hidden("LanguageStrings[0].Another")
    @Html.Hidden("LanguageStrings[1].Val", "test2")
    @Html.Hidden("LanguageStrings[1].Another")
    @Html.Hidden("LanguageStrings[2].Val", "test3")
    @Html.Hidden("LanguageStrings[2].Another")

    @Html.Hidden("LanguageStringsGroup[0].Val", "test4")

    @Html.Hidden("Deleted[0]")
    @Html.Hidden("Deleted[1]")
    @Html.Hidden("Deleted[2]")

    @Html.Hidden("DeletedGroup[0]")

    <button>Post</button>
}

It also works when I remove the "Deleted" list. Like this:

<h2>Test</h2>

@using (Html.BeginForm("Post", "Test"))
{
    @Html.Hidden("LanguageStrings[0].Val", "test1")
    @Html.Hidden("LanguageStrings[0].Another")
    @Html.Hidden("LanguageStrings[1].Val", "test2")
    @Html.Hidden("LanguageStrings[1].Another")

    @Html.Hidden("LanguageStringsGroup[0].Val", "test4")

    @Html.Hidden("DeletedGroup[0]")

    <button>Post</button>
}

This has something to do with the naming I am using. I have already solved the problem with renaming LanguageStrings to something else. But I would like to understand what is happening here because probably I could learn something from it how MVC maps request body and will be able to avoid similar time consuming problems. Please help me and explain the cause of this.

Upvotes: 6

Views: 848

Answers (2)

LostInComputer
LostInComputer

Reputation: 15420

You found a bug in the PrefixContainer of MVC 4 which has already been fixed in MVC 5.

Here is the fixed version with comments about the bug:

internal bool ContainsPrefix(string prefix)
{
    if (prefix == null)
    {
        throw new ArgumentNullException("prefix");
    }

    if (prefix.Length == 0)
    {
        return _sortedValues.Length > 0; // only match empty string when we have some value
    }

    PrefixComparer prefixComparer = new PrefixComparer(prefix);
    bool containsPrefix = Array.BinarySearch(_sortedValues, prefix, prefixComparer) > -1;
    if (!containsPrefix)
    {
        // If there's something in the search boundary that starts with the same name
        // as the collection prefix that we're trying to find, the binary search would actually fail.
        // For example, let's say we have foo.a, foo.bE and foo.b[0]. Calling Array.BinarySearch
        // will fail to find foo.b because it will land on foo.bE, then look at foo.a and finally
        // failing to find the prefix which is actually present in the container (foo.b[0]).
        // Here we're doing another pass looking specifically for collection prefix.
        containsPrefix = Array.BinarySearch(_sortedValues, prefix + "[", prefixComparer) > -1;
    }
    return containsPrefix;
}

Upvotes: 5

Adrian
Adrian

Reputation: 222

I have had much more success with @Html.HiddenFor() for posting back to the controller. Code would look something like this:

@for (int i = 0; i < @Model.LanguageStrings.Count; i++)
{
    @Html.HiddenFor(model => model.LanguageStrings[i].Val, string.Format("test{0}", i))
    @Html.HiddenFor(model => model.LanguageStrings[i].Another)
}

Most HTML helper methods have a "For" helper that is intended to be used for binding data to models. Here is another post on the site that explains the "For" methods well: What is the difference between Html.Hidden and Html.HiddenFor

Upvotes: 0

Related Questions