Search code examples
c#asp.net-corerazor-pagesasp.net-core-viewcomponent

Model Binding a View Component in Razor Pages


I'm using ASP.NET Core 2.1, Razor Pages, and a View Component (though I'm not sold if View Components are needed yet)

I have a parent Model, Organization, that contains a list of child Models, Contacts:

Organization

public Guid Id { get; set; }
public string OrganizationName { get; set; }

public ICollection<Contact> Contacts { get; set; }

Contact

public Guid Id { get; set; }
public string ContactName { get; set; }
public Guid OrganizationId { get; set; }

What I'm trying to do is have a list of n number of Contacts in Organization that I can add and delete from a View and have that reflected when I hit my OnPost() Page Model method


In my previous project, an MVC5 web app:

The combination of those two looks a lot like this: MVC 5 BeginCollectionItem with Partial CRUD


Current status

In ~/ViewComponents/Contact.cs I have:

public class Contact : ViewComponent
{
    private readonly ApplicationDbContext _context;

    public Contact(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<IViewComponentResult> InvokeAsync(Guid organizationId, Guid contactId)
    {
        var contact = new Models.Contact { Id = contactId, OrganizationId = organizationId };
        return View(contact);
    }
}

Which invokes ~/Pages/Shared/Components/Contact/Default.cshtml, uses a ported version of BeginCollectionItem:

@model Models.Contact

@using (Html.BeginCollectionItem("Contacts"))
{
    <div class="row">
        <input asp-for="Id" type="hidden" />
        <input asp-for="OrganizationId" type="hidden"/>
        <input asp-for="ContactName" class="form-control"/>
    </div>
}

I'm then directly loading this in Organiztion's ~/Pages/Organizations/Create.cshtml:

@page
@using System.Linq
@using ViewComponents
@model CreateModel

@{
    ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Organization</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div class="form-group">
                <input asp-for="Organization.OrganizationName" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="OrganizationContacts" class="col-form-label"></label>
  <!-- HERE --> @await Component.InvokeAsync(nameof(ViewComponents.Contact), new { organizationId = Model.Organization.Id, contactId = Guid.NewGuid() })
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

When I click the Submit button, it hits the OnPostAsync() method in Create.cshtml.cs:

public class CreateModel : PageModel
{
    private readonly ApplicationDbContext _context;

    [BindProperty]
    public Organization Organization { get; set; }

    public CreateModel(ApplicationDbContext context, IMapper mapper)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        Organization = new Organization{ Contacts = new List<Contact>() };
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        _context.Organizations.Add(Organization); // <--- Always has an empty Contacts list
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

When I hit OnPostAsync, the bound Organization Model has it's OrganizationName value just fine, but the Contacts list is always empty.

How can I add a Contact to Organization's Contacts list knowing that later I want to have n number of Contacts added or removed?

Most answers I've seen to this kind of question have for loops, but they require me to know ahead of time how many Contacts are or will be in an Organization and that simply isn't the case for me.


Solution

  • For resolving empty Contacts, try to change Html.BeginCollectionItem("Contacts") to Html.BeginCollectionItem("Organization.Contacts").

    Since Contacts is collection of Organization, you need to pass Organization.Contacts[index].Property, otherwise, it will fail to bind the request to model.