2

Short overview

I have a very curious bug and I have absolutetly no idea where it is coming from. I want to create a webpage, where you can modify a table and out of this table you can generate a table in the database. For that I am using ASP .NET MVC 4 and I am already able to modify and create the table in the database, but I have a small "+" button, where I want to insert cells into the table (model).

Problem

It already works, but it is inserting it always at the last index AND the name is also equal to the one, which was last before. (same for deleting with "-" button)

Here two screenshots:
Before clicking: before After clicking: after

Code

To achieve that all I have created two models:

CreateTableModel

I initialize the Model with one column. (I have more proerties and functions which are not relevant)

public class CreateTableModels
{
    public CreateTableModels()
    {
        Columns = new List<CreateTableColumnModels>() { new CreateTableColumnModels(){ PrimaryKey = true } };
    }

    [Required]
    [UniqueNamesValidation(ErrorMessage="Alle Namen müssen unique sein")]
    [Display(Name = "Columns")]
    public List<CreateTableColumnModels> Columns { get; private set; }

    //more properties...

This Model has a list of CreateTableColumnModels:

public class CreateTableColumnModels
{
    public CreateTableColumnModels()
    {
        Name = "name";
        NotNull = false;
        PrimaryKey = false;
        Length = 1;
    }

    [Required]
    [Display(Name = "Typ")]
    public System.Data.SqlDbType Type { get; set; }

    [Key]
    [Required(ErrorMessage = "Darf nicht leer sein!")]
    [RegularExpression(@"^[A-Za-z0-9]+$", ErrorMessage = "Nur Buchstaben und Zahlen sind erlaubt")]
    [Display(Name = "Name")]
    public string Name { get; set; }

    //more properties...

Controller

In the controller i want to insert a column to the model (if the button was pressed it sends the index via addCell). The column should have the name "Added". If I Add a new column it works fine.

public class CreateTableController : Controller
{
    public ViewResult Index(CreateTableModels model, int count = 1, int? addCell = null, int? removeCell = null, bool createTable = false)
    {
        if (addCell != null)
        {
            model.Columns.Insert(addCell.Value, new CreateTableColumnModels() { Name = "ADDED", Type = System.Data.SqlDbType.Float });
        }

View

I did put nearly the whole view here, because I have absolutely no clue where the problem is located.

<h2>CreateTable</h2>

@using (Html.BeginForm("Index", "CreateTable", FormMethod.Get))
{
@Html.AntiForgeryToken()

<div class="form-horizontal">
    @Html.ValidationSummary(true)
</div>
<table id="table" class="table">
    <thead>
        <tr>
            <th>@Html.LabelFor(model => model.Columns.FirstOrDefault().Type)</th>
            <th>@Html.LabelFor(model => model.Columns.FirstOrDefault().Length)</th>
            <th>@Html.LabelFor(model => model.Columns.FirstOrDefault().Precision)</th>
            <th>@Html.LabelFor(model => model.Columns.FirstOrDefault().Name)</th>
            <th>@Html.LabelFor(model => model.Columns.FirstOrDefault().NotNull)</th>
            <th>@Html.LabelFor(model => model.Columns.FirstOrDefault().PrimaryKey)</th>
        </tr>
    </thead>
    <tbody>
        @for (int i = 0; i < @Model.Columns.Count; i++)
        {
            <tr>
                <td>
                    <div id="TableSelection">
                        @Html.DropDownListFor(m => Model.Columns[i].Type, new SelectList(Model.Columns[i].Types), new { @ID = "typeSelection", @class = "form-control", @style = "width:150px;" })
                    </div>
                </td>
                <td>
                    <div id="Length">
                        @Html.TextBoxFor(m => Model.Columns[i].Length, new { @ID = "length", @class = "form-control", @type = "number", @style = "width:80px", @disabled = "disabled" })
                        @Html.ValidationMessageFor(m => Model.Columns[i].Length)
                    </div>
                </td>
                <td>
                    <div id="Precision">
                        @Html.TextBoxFor(m => Model.Columns[i].Precision, new { @ID = "precision", @class = "form-control", @type = "number", @style = "width:80px", @disabled = "disabled" })
                        @Html.ValidationMessageFor(m => Model.Columns[i].Precision)
                    </div>
                </td>
                <td>
                    <div id="Name">
                        @Html.TextBoxFor(model => Model.Columns[i].Name, new { @class = "form-control", @style = "width:150px" })
                        @Html.ValidationMessageFor(model => Model.Columns[i].Name)
                    </div>
                </td>
                <td>
                    <div id="NotNull">
                        @Html.CheckBoxFor(model => Model.Columns[i].NotNull, new { @ID = "notNull", @checked = Model.Columns[i].NotNull, @class = "checkbox" })
                    </div>
                </td>
                <td>
                    <div id="PrimaryKey">
                        @Html.CheckBoxFor(x => Model.Columns[i].PrimaryKey, new { @ID = "primaryKey", @checked = Model.Columns[i].PrimaryKey, @class = "checkbox" })
                    </div>
                </td>
                <td>
                    <div>
                        <button type="submit" class="btn btn-default" name="addCell" value="@i">+</button>
                        <button type="submit" class="btn btn-default" name="removeCell" value="@i">-</button>
                    </div>
                </td>
            </tr>
        }
    </tbody>
</table>

<div align="left" class=" form-group" style="display: flex; flex-direction: row; justify-content: flex-start; align-items: center;">
    <div>
        @Html.LabelFor(model => model.TableName, new { @class = "control-label col-md-2" })
    </div>
    <div class="text-center control-form">
        @Html.EditorFor(model => model.TableName, new { @class = "text-center control-form" })
        @Html.ValidationMessageFor(model => model.TableName)
    </div>
    <div>
        <button type="submit" style="margin-left:10px; margin-right:10px;" name="createTable" value="true" class="btn btn-success">Create</button>
    </div>
    <div>
        @if (Model.WorkDone != null && (bool)Model.WorkDone)
        {
            <label style="font-size:medium" class="label label-success">Alle Änderungen wurden gespeichert!</label>
        }
        else if (Model.WorkDone != null)
        {
            <label style="font-size:medium" class="label label-danger">Es ist etwas schief gelaufen!</label>
        }
    </div>
</div>
}

Debug

Also if I debug it till the end of the view, it has the right values. debug

The only thing I can imagine changing it is the @RenderBody() in the _Layout.cshtml file. Is there anyone who had something similar or can help me? Already thanks for reading to the end :)

David Walser
  • 183
  • 20
  • MVC renders POSTed values instead of values assigned in the controller. – CodeCaster Jul 18 '18 at 12:21
  • @CodeCaster Which posted values exactly do you mean? I have checked all GET parameters and they seemed OK. – David Walser Jul 18 '18 at 12:41
  • Before adding an answer (the issue is related to `ModelState`), can you confirm that `public List Columns { get; private set; }` is a typo? - you have a private setter which means that none of your posted values are bound (because the `DefaultModelBinder` cannot **set** them), and you model in the POST method will only contain 1 new item (the one you added in the constructor). I assume that it must actually be `public List Columns { get; set; }` otherwise you would only see 2 items when the view is returned –  Aug 09 '18 at 10:05
  • @StephenMuecke I made the setter publi, but nothing changed. As shown in the Debug image, The model has the right columns (even in the view). – David Walser Aug 09 '18 at 10:55

1 Answers1

2

The issue is that when you submit your collection, the posted values are added to ModelState. If we look at just the Name property, the following values are added

Columns[0].Name // value = "a"
Columns[1].Name // value = "b"
Columns[2].Name // value = "c"

Now you insert a new column (say before the first item). The values in the model are now

Columns[0].Name // value = "ADDED"
Columns[1].Name // value = "a"
Columns[2].Name // value = "b"
Columns[3].Name // value = "c"

Now when you return the view, you (correcty) use the HtmlHelper methods to generate the view, but all the HtmlHelper methods that generate form controls render the value attribute by checking the following (in order)

  1. the value in ModelState (if any)
  2. the value in the ViewDataDictionary (if any)
  3. the value from the model

The reason for this behavior is explained in TextBoxFor displaying initial value, not the value updated from code.

In the first iteration of the for loop, your @Html.TextBoxFor(m => m.Columns[i].Name) sees a value of "a" in ModelState and renders "a" in the textbox (even though the value of the model is "ADDED").

In the 2nd iteration, the value is again taken from ModelState, so "b" is rendered.

In the 3rd iteration, the value is again taken from ModelState, so "c" is rendered.

And in the 4th and final iteration, a value is not found in ModelState (or ViewData) for Columns[3].Name, so it reads the value from the model, so "c" is rendered.

As a side note, if you added @Model.Columns[i].Name in a table cell, you would see the values rendered in accordance with the property value i.e. Row 1: "ADDED", Row 2: "a", Row 3: "b", Row4: "c".

To solve this, you can add ModelState.Clear() before your return the view. However, the side effect of this is that it also clears any validation errors, so at the least, you should immediately return the view if ModelState is not valid

public ViewResult Index(CreateTableModels model, ...)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    ... // code to insert new row
    ModelState.Clear();
    return View(model);
}

However, the best approach is to follow the PRG pattern and redirect to a method that generates the new data.