Issues with binding multiple objects on a razor page

c# entity-framework-core razor-pages

Question

I am trying to build a razor page that allows the user to edit a number of different objects on a single page - specific tests to be run in a lab under a single test run. I have been running around in circles, and have now got to a point where: 1. Posting the page back seems to clear some data on the parent (testRun) object on the page, but the data remains intact when reloaded 2. Changes to the child object (testRowFatContent) are retained on the page, but are not saved on the database, and disappear when the page is reloaded.

My aim is for the user to be able to edit any of the objects on the page, click save, and have the values returned to the screen and saved in the database.

I have tried forcing the entity framework state to modified, but this doesn't really seem like the best solution (as I will need to create hidden input fields on the cshtml page to capture the full object), and I am unsure how to set a list of objects to be marked as modified. I suspect I am missing something fundamental with the original data binding, but can't figure it out after trawling the Microsoft pages and stackoverflow. I did remove a @foreach loop and replaced it with a @for loop with index within the cshtml after reading some articles that indicated that may be the issue.

namespace KookaburraLab.Pages.TestRun
{
    public class RunTests : PageModel
    {
        private readonly KookaburraLab.Models.KookaburraLabContext _context;

        public RunTests(KookaburraLab.Models.KookaburraLabContext context)
        {
            _context = context;
        }
        [BindProperty]
        public KookaburraLab.Models.TestRun TestRun { get; set; }
        [BindProperty]
        public List<TestRowFatContent> testRowFatContents { get; set; }
        [BindProperty]
        public List<TestRowSaltExtract> testRowSaltExtracts { get; set; }
        [BindProperty]
        public List<TestRowWaterAbsorption> testRowWaterAbsorptions { get; set; }
        [BindProperty]
        public List<TestRowTensileStrength> testRowTensileStrength { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            TestRun = await _context.TestRun.SingleOrDefaultAsync(m => m.TestRunID == id);

            if (TestRun == null)
            {
                return NotFound();
            }

            testRowFatContents = await _context.TestRowFatContent.Where(t => t.TestRunID == id).ToListAsync();
            testRowWaterAbsorptions = await _context.TestRowWaterAbsorption.Where(t => t.TestRunID == id).ToListAsync();
            testRowSaltExtracts = await _context.TestRowSaltExtract.Where(t => t.TestRunID == id).ToListAsync();
            testRowTensileStrength = await _context.TestRowTensileStrength.Where(t => t.TestRunID == id).ToListAsync();
            return Page();
        }

        public async Task<IActionResult> OnPostSaveChangesAsync()
        {

            _context.SaveChanges(); 
            //ModelState.Clear();
            testRowFatContents = await _context.TestRowFatContent.Where(t => t.TestRunID == TestRun.TestRunID).ToListAsync();
            testRowWaterAbsorptions = await _context.TestRowWaterAbsorption.Where(t => t.TestRunID == TestRun.TestRunID).ToListAsync();
            testRowSaltExtracts = await _context.TestRowSaltExtract.Where(t => t.TestRunID == TestRun.TestRunID).ToListAsync();
            testRowTensileStrength = await _context.TestRowTensileStrength.Where(t => t.TestRunID == TestRun.TestRunID).ToListAsync();

            return Page();
        }

        public async Task<IActionResult> OnPostCloseTestAsync()
        {
            return Page();
        }
    }
}
 <form method="post" enctype="multipart/form-data">

        <button type="submit" class="btn btn-success btn-sm" asp-page-handler="SaveChanges">Save Changes</button>
        <button type="submit" class="btn btn-danger btn-sm" asp-page-handler="CloseTest">Abandon Test Run</button>
        <input type="hidden" asp-for="TestRun.TestRunID" />
        <input type="hidden" asp-for="TestRun.TestItemCreatedUser" />
        @*<div>
            <partial name="_TestRowFatContent"/>
        </div>*@

        <div>
            @if (Model.testRowFatContents.Count > 0)
    {
            <h4>Fat Content Test</h4>
            <table class="table">
                <thead>
                    <tr>
                        <th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].TestRowStatus)
                        </th>

                        <th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].TestRowSampleDescription)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].w1EmptyFlask)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].w2ThimbleWeight)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].w3ThimbleLeatherWeight)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].w4LeatherWeight)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].w5FlaskExtractWeight)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].w6ExtractWeight)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].FatContent)
                        </th>
                        @*<th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].TestItemID)
                        </th>*@
                        <th>
                            @Html.DisplayNameFor(model => model.testRowFatContents[0].TestRowSampleID)
                        </th>

                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    @for (int i=0; i < Model.testRowFatContents.Count(); i++)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => Model.testRowFatContents[i].TestRowStatus)
                        </td>
                        <td>
                            <input asp-for="@Model.testRowFatContents[i].TestRowSampleDescription" class="form-control" />
                            <input type="hidden" asp-for="@Model.testRowFatContents[i].TestRowID" />

                        </td>
                        <td>
                            <input asp-for="@Model.testRowFatContents[i].w1EmptyFlask" class="form-control" />
                        </td>
                        <td>
                            <input asp-for="@Model.testRowFatContents[i].w2ThimbleWeight" class="form-control" />
                        </td>
                        <td>
                            <input asp-for="@Model.testRowFatContents[i].w3ThimbleLeatherWeight" class="form-control" />
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => @Model.testRowFatContents[i].w4LeatherWeight)
                        </td>
                        <td>
                            <input asp-for="@Model.testRowFatContents[i].w5FlaskExtractWeight" class="form-control" />
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => @Model.testRowFatContents[i].w6ExtractWeight)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => @Model.testRowFatContents[i].FatContent)
                        </td>
                        @*<td>
                            @Html.DisplayFor(modelItem => item.TestItemID)
                        </td>*@
                        <td>
                            @Html.DisplayFor(modelItem => @Model.testRowFatContents[i].TestRowSampleID)
                        </td>

                        @*<td>
                            <a asp-page="./Edit" asp-route-id="@item.TestRowID">Edit</a> |
                            <a asp-page="./Details" asp-route-id="@item.TestRowID">Details</a> |
                            <a asp-page="./Delete" asp-route-id="@item.TestRowID">Delete</a>
                        </td>*@
                    </tr>
}

Any help would be greatly appreciated - apologies in advance if I have missed something obvious.

While debugging I have confirmed that the OnPost handler is being called, and the modelstate is reflecting the new (changed) values correctly - it just seems to lose the dbcontext, which I thought was meant to be maintained across GET and POST calls in .NET core. I really want to avoid having to manually specify the update to the dbcontext for every specific type, as I will potentially have a large number of items using this page. I also attempted to use AttachRange(testFatContents) which did not seem to work, regardless of whether I included this in the GET or POST handler (or both!)

I have a similarly structured page that adds new items to the dbcontext that works quite well,

1
0
7/18/2019 12:19:52 AM

Accepted Answer

You are calling _context.SaveChanges() which is the correct way to save changes on your DB, but you are missing to tell the DB what you want to change.

You'd want to use Model binding to collect the Form data that your user is sending to the server.

public async Task<IActionResult> OnPostSaveChangesAsync(List<TestRowFatContent> testRowFatContents, List<TestRowSaltExtract> testRowSaltExtracts, List<TestRowWaterAbsorption> testRowWaterAbsorptions, List<TestRowTensileStrength> testRowTensileStrength)
{
    //Check if your list objects are filled with the user input, the name attribute of the input fields need to match the variable names above

    //tell the context what you want to update 
    _context.UpdateRange(testRowFatContents);
    _context.UpdateRange(testRowSaltExtracts);
    _context.UpdateRange(testRowWaterAbsorptions);
    _context.UpdateRange(testRowTensileStrength);
    _context.SaveChanges(); 

    return Page();
}

Updating in Entity Framework is kind of tricky sometimes, but the important part is that you have the data in your SaveChanges() method and then tell your database what to do with it.

If you have fields that are not included in the form, they will be overwritten, as the EF just saves exactly the object you get from your model binding. To avoid unused fields to be overwritten, you could either include hidden input fields or use EF's IsModified attribute. You have to set it on the attributes that you don't want to be changed.

Example:

_context.Entry(testRowFatContents).Property("FatContent").IsModified = false;
1
7/19/2019 8:59:16 AM

Popular Answer

When the form is submitted, the form data is bound correctly to the PageModel properties, but those changes are not applied to the context. You are working in a disconnected scenario - the state within the page - including the context - is not maintained across requests.It is reconstructed each time the page is executed. When the page is executed as a result of a POST request, the state is constructed by the modelbinder. Therefore, you have to attach the reconstructed (changed) entities to the context, and tell the context what state they are in (modified). Then the context will know what changes to apply.

EF Core provides an UpdateRange method that is designed to let the context know that a collection has been modified e.g.

_context.UpdateRange(testRowFatContents);
await _context.SaveChangesAsync();

See more about working in a disconnected scenario here: https://www.learnentityframeworkcore.com/dbcontext/modifying-data#disconnected-scenario



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow