1
votes

I'm not understanding how to properly bind a single item from a collection. In my project, a student can have multiple plans with these models:

public class Student
{
    public Student()
    {
        Plans = new HashSet<Plan>();
    }

    public int StudentId { get; set; }
    public string Designee { get; set; }
    public string DesigneePhone { get; set; }

    public virtual ICollection<Plan> Plans { get; set; }
}

public partial class Plan
{
    public int PlanId { get; set; }

    public int StudentId { get; set; }

    public DateTime? StartDate { get; set; }
    public DateTime? EndDate { get; set; }

    public virtual Student Student { get; set; }
}

I'm listing each plan with its own Save button on the page. I thought I was using the asp-for helper correctly according to this Learn Razor Pages post.

@page "{studentId:int}"
@model CollectionTest.PlansModel

<form asp-page-handler="SaveMain" method="post">
    <div class="row mt-3">
        <input type="hidden" asp-for="Student.StudentId" />
        <div class="col">
            <label asp-for="Student.Designee"></label>
            <input asp-for="Student.Designee" class="form-control" />
        </div>
        <div class="col">
            <label asp-for="Student.DesigneePhone"></label>
            <input asp-for="Student.DesigneePhone" class="form-control" />
        </div>
        <div class="col-auto align-self-end">
            <input type="submit" value="Save" class="btn btn-primary" />
        </div>
    </div>
</form>

<hr />

@for (int i = 0; i < Model.Student.Plans.Count(); i++)
{
    var planId = Model.Student.Plans.ToList()[i].PlanId.ToString();

    <div id="@("card_Plan" + planId)" class="card">
        <div class="card-header">
            <h5>Plan #@planId</h5>
        </div>
        <div class="card-body">
            <form id="@("form_Plan" + planId)" asp-page-handler="SavePlan" method="post">
                <input type="hidden" asp-for="Student.Plans.ToList()[i].PlanId" />
                <div class="row">
                    <div class="col">
                        <label asp-for="Student.Plans.ToList()[i].StartDate"></label>
                        <input asp-for="Student.Plans.ToList()[i].StartDate" class="form-control" />
                        <span asp-validation-for="Student.Plans.ToList()[i].StartDate"></span>
                    </div>
                    <div class="col">
                        <label asp-for="Student.Plans.ToList()[i].EndDate"></label>
                        <input asp-for="Student.Plans.ToList()[i].EndDate" class="form-control" />
                        <span asp-validation-for="Student.Plans.ToList()[i].EndDate"></span>
                    </div>
                </div>
            </form>
        </div>
        <div class="card-footer">
            <div class="float-right">
                <button type="submit" class="btn btn-primary" form="@("form_Plan" + planId)">Save</button>
            </div>
        </div>
    </div>
}

But it seems like nothing is being bound in the SavePlan page handler:

[BindProperty]
public Student Student { get; set; }
.
.
.
public async Task<IActionResult> OnPostSavePlan(int planId)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    /******************************
     *
     * Student.Plans is empty and planId is zero
     *
     *******************************/
    Plan plan = await _planContext.Plan.FirstAsync(p => p.PlanId == planId);

    _planContext.Attach(plan).State = EntityState.Modified;

    try
    {
        await _planContext.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!PlanExists(plan.PlanId))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Plans");
}

What am I doing wrong?

1

1 Answers

2
votes

According to your code, because what you pass in the razor page is [0].PlanId, but what you receive in the OnPostSavePlan method is planId. If you only need to receive planId, you need to change the form.

<div class="card-body">
        <form id="@("form_Plan" + planId)" asp-page-handler="SavePlan" method="post">
            <input type="hidden" asp-for="@planId" />

From your requirement,it seems you want to pass a list of plans. So you need to modify your code in the following steps:

Plans.cshtml:

<form id="form_Plan" asp-page-handler="SavePlan" method="post">
    @for (int i = 0; i < Model.Student.Plans.Count(); i++)
    {
        var planId = Model.Student.Plans.ToList()[i].PlanId.ToString();

        <div id="@("card_Plan" + planId)" class="card">
            <div class="card-header">
                <h5>Plan #@planId</h5>
            </div>
            <div class="card-body">
                <input type="hidden" asp-for="Student.Plans.ToList()[i].PlanId" />
                <div class="row">
                    <div class="col">
                        <label asp-for="Student.Plans.ToList()[i].StartDate"></label>
                        <input asp-for="Student.Plans.ToList()[i].StartDate" class="form-control" />
                        <span asp-validation-for="Student.Plans.ToList()[i].StartDate"></span>
                    </div>
                    <div class="col">
                        <label asp-for="Student.Plans.ToList()[i].EndDate"></label>
                        <input asp-for="Student.Plans.ToList()[i].EndDate" class="form-control" />
                        <span asp-validation-for="Student.Plans.ToList()[i].EndDate"></span>
                    </div>
                </div>
            </div>
        </div>
    }
    <div class="card-footer">
        <div class="float-right">
            <button type="submit" class="btn btn-primary" form="form_Plan">Save</button>
        </div>
    </div>
</form>

Plans.cshtml.cs:

public class PlansModel : PageModel
{
    public IActionResult OnGet()
    {
        //for easy testing,I add the data manually
        Student = new Student()
        {
            Plans = new List<Plan>()
            {
                new Plan(){  PlanId=1, StartDate=DateTime.Parse("2019-8-7")},
                new Plan(){  PlanId=2, StartDate=DateTime.Parse("2019-4-7")},
                new Plan(){  PlanId=3, StartDate=DateTime.Parse("2019-6-7")}
            }
        };
        return Page();
    }

    [BindProperty]
    public Student Student { get; set; }
    public async Task<IActionResult> OnPostSavePlan(List<Plan> plans)
    {
        //...
    }
}

Result: enter image description here

Update:

Plans.cshtml:

@foreach (var plan in Model.Student.Plans)
{
    var planId = plan.PlanId.ToString();
    <div id="@("card_Plan" + planId)" class="card">
        <div class="card-header">
            <h5>Plan #@planId</h5>
        </div>
        <div class="card-body">
            <form id="@("form_Plan" + planId)" asp-page-handler="SavePlan" method="post">
                <input type="hidden" asp-for="@plan.PlanId" />
                <div class="row">
                    <div class="col">
                        <label asp-for="@plan.StartDate"></label>
                        <input asp-for="@plan.StartDate" class="form-control" />
                        <span asp-validation-for="@plan.StartDate"></span>
                    </div>
                    <div class="col">
                        <label asp-for="@plan.EndDate"></label>
                        <input asp-for="@plan.EndDate" class="form-control" />
                        <span asp-validation-for="@plan.EndDate"></span>
                    </div>
                </div>
            </form>
        </div>
        <div class="card-footer">
            <div class="float-right">
                <button type="submit" class="btn btn-primary" form="@("form_Plan" + planId)">Save</button>
            </div>
        </div>
    </div>
}

Plans.cshtml.cs:

public async Task<IActionResult> OnPostSavePlan(Plan plan)
{
    //...
}

Result: enter image description here