0

I have a view, whose Model is a ViewModel, which is used to display some data. For each line displayed this way, the user can choose a value from a DropDownList, then click a button to save their choice into the database. More than one "choice" can be processed this way per button click.

In order to do so, I use an Html.BeginForm method, like so (named altered):

        @using (Html.BeginForm("DatabaseSave", "Save", new { attr = Model.attributedList }, FormMethod.Post))

And my method in the controller is:

[HttpPost]
[ValidateAntiForgeryToken]       
public ActionResult DatabaseSave(AttributionViewModel attr)
{
   // Data processing.
}

But when I use this, attr is null. I can still get my data by using Request.Form and the correct index, but that is not the solution I'd rather use. I have a model that is ready to do this and used a ViewModel specifically so I didn't have to ask Request.Form. But I just can't see what I'm missing. Can anybody help me?

In order for an answer to be found quickly, you will find the pseudocode of all relevant classes and views below.

Class that contains the data:

public class Attribution
{
    public Attribution()
    {
        listToAttribute = new List<SelectListItem>();
    }
    public string Title { get; set; }
    public int ID { get; set; }
    public string Description { get; set; }
    public int? Number { get; set; }
    public IEnumerable<SelectListItem> listToAttribute { get; set; }
    public int AttributedNumber { get; set; } // Mostly used internally to properly send dropdownlist data.
    // Other methods that are not relevant.
    }

The ViewModel used in the View:

public class AttributionViewModel : Attribution
{
    public AttributionViewModel()
    {

    }
    public IPagedList<Attribution> attributedList { get; set; }
}

The relevant parts of the view:

 @model Project.ViewModels.AttributionViewModel 
 @using PagedList.Mvc
 @using PagedList

// Some code later...

            @using (Html.BeginForm("DatabaseSave ", "Save", new { attr = Model.attributedList }, FormMethod.Post))
        {
                        @Html.ValidationSummary(false, "", new { @class = "text-danger" })
            @Html.AntiForgeryToken()
            <table class="contentTable contentTable-evo fixedTableHeader">
                <thead>
                   // Headers are not relevant.
                </thead>
                <tbody id="processingTable">
                    @{
                        int i = 0;
                    }
                    @foreach (var item in Model.attributedList)
                    {

                        <tr>
                            <text>@Html.EditorFor(modelItem => Model.attributedList[i].ID, new { htmlAttributes = new { @class = "form-control", @hidden = "hidden", @style = "width:0px display:none", @type = "hidden" } })</text>
                            <td class="noWrap width1percent tri">@item.Title</td>
                            <td class="noWrap width1percent tri">@item.ID</td>
                            <td class="noWrap width1percent tri">@item.Description</td>
                            <td class="noWrap width1percent tri">@item.Number</td>
                            <td class="noWrap width1percent tri">
                                <text>@Html.DropDownListFor(modelItem => Model.attributedList[i].AttributedNumber, Model.attributedList[i].listToAttribute , "" , new { htmlAttributes = new { @class = "form-control", @style = "width:90px" } })</text>
                            </td>
                        </tr>
                        i++;
                    }
                </tbody>
            </table>
            <br />
            <center>
                <input value="Processing" class="add" type="submit" onclick="javascript:return ShowConfirm();" >
            </center>

            Page @(Model.attributedList.PageCount < Model.attributedList.PageNumber ? 0 : Model.attributedList.PageNumber) out of @Model.attributedList.PageCount | @Model.attributedList.TotalItemCount <br />
        <text>@Resources.Views.PaginationResource.GoToPage </text>@Html.DropDownList("PageChanger", new SelectList(Enumerable.Range(1, Model.attributedArticles.PageCount), "", ""), new { onchange = "PageChangedFromDropDown()" })

        @Html.PagedListPager(Model.attributedList, page => Url.Action("Attribute", new { page, searchString = Session["Search"], currentFilter = ViewBag.CurrentFilter, sortColumn = ViewBag.sortAttr }), new PagedListRenderOptions { LiElementClasses = new[] { "needsLoading" }, DisplayEllipsesWhenNotShowingAllPageNumbers = false })
  • Start by removing `new { attr = Model.attributedList }` from the `BeginForm`. –  Jun 21 '18 at 06:24
  • If I do, I get an "System.MissingMethodException: cannot create an instance of an interface" exception (upon submitting the form). I put it there in the first place because I was getting this error before. – Frédéric Liard Jun 21 '18 at 06:26
  • That is a separate issue (it need to be removed - look at the form attribute your generating - the `action` attribute to understand why it would never bind) –  Jun 21 '18 at 06:30
  • And why are you using `IPagedList`? (I assume that is the the cause of the other error) –  Jun 21 '18 at 06:31
  • I'm using PagedList because I have to paginate this specific view; I have added the relevant code in the view, at the very bottom. The PagedList is the pagination solution used for the entire project. – Frédéric Liard Jun 21 '18 at 06:36
  • Furthermore, I have identified that the MissingMethodException exception actually comes from using "Model.attributedList[i].ID" and "Model.attributedList[i].AttributedNumber". If I change these to "modelItem.ID" and "modelItem.AttributedNumber", then remove the new {attr = Model.attributedList}, I do not get this error; but the returned object is still empty, or only takes the first change into account. – Frédéric Liard Jun 21 '18 at 06:55
  • 1
    Sorry, but that makes no sense - `IPagedList` is for reading, not editing and it does not work because you are trying to initialize `IPageList` which wont work. One possible option would be to post back to a different model containing the same properties except the collection would be `LIst` –  Jun 21 '18 at 06:56
  • You cannot use `modelItem.ID` etc - that is creating form controls that have no relationship to your model. Refer [this answer](http://stackoverflow.com/questions/30094047/html-table-to-ado-net-datatable/30094943#30094943) to understand how to generate form controls for a collection –  Jun 21 '18 at 06:58
  • I got it! Your explanation about the PagedList made it click for me. I needed to create a list that was NOT a PagedList and use that instead. I'll post the solution below and validate that, unless you want to post your own. Thanks for your help! – Frédéric Liard Jun 21 '18 at 07:09
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/173529/discussion-between-stephen-muecke-and-frederic-liard). –  Jun 21 '18 at 07:10
  • I would, but my work firewall blocks it as a chat. – Frédéric Liard Jun 21 '18 at 07:11

1 Answers1

0

Thanks to Stephen Muecke I managed to find what I was doing wrong.

The problem was in the ViewModel:

    public IPagedList<Attribution> attributedList { get; set; }

I needed to use that because a ViewModel cannot normally use a PagedList.

However, that doesn't mean it can be used to contain and parse data. I needed a separate list for that, adding this:

    public IPagedList<Attribution> attributedList { get; set; }
    public List<Attribution> parsableListOfAttributed { get; set; } // Otherwise passing to the controller WILL NOT WORK.

For any manipulation that has to do with the PagedList (in my example above, I need it to count the pages, for instance) I can refer to attributedList. But this will not work when passing it to the Controller, who still considers it a PagedList, and therefore cannot be used for that purpose.

Corrected view:

        @using (Html.BeginForm("DatabaseSave", "Save", FormMethod.Post))
        {
                        @Html.ValidationSummary(false, "", new { @class = "text-danger" })
            @Html.AntiForgeryToken()
            <table class="contentTable contentTable-evo fixedTableHeader">
                <thead>
                    // Headers are still not relevant.
                </thead>
                <tbody id="processingTable">
                    @{
                        int i = 0;
                    }
                    @foreach (var item in Model.attributedArticles)
                    {

                        <tr>
                            <text>@Html.EditorFor(modelItem => Model.parsableListOfAttributed[i].ID, new { htmlAttributes = new { @class = "form-control", @hidden = "hidden", @style = "width:0px display:none", @type = "hidden" } })</text>
                            <td class="noWrap width1percent tri">@item.GTIN</td>
                            <td class="noWrap width1percent tri">@item.SMP</td>
                            <td class="noWrap width1percent tri">@item.DescriptionFr</td>
                            <td class="noWrap width1percent tri">@item.AuthorisationNumber</td>
                            <td class="noWrap width1percent tri">
                                <text>@Html.DropDownListFor(modelItem => Model.parsableListOfAttributed[i].AttributedNumber, Model.parsableListOfAttributed[i].listToAttribute, "" , new { htmlAttributes = new { @class = "form-control", @style = "width:90px"} })</text>
                            </td>
                        </tr>
                        i++;
                    }
                </tbody>
            </table>
            <br />
            <center>
                <input value="Processing"" class="add" type="submit" onclick="javascript:return ShowConfirm();" >
            </center>

                        }

The model is correctly passed using this method.