Search code examples
c#.netasp.net-core-mvcdomain-driven-design.net-6.0

ASP.NET Core 6 MVC page to create object with list of complex objects


I have the following model, which needs to be created via a page:

public class Exercise
{
    public Guid Id{ get; set; }
    public string Name { get; set; }
    public string Description{ get; set; }

    public ICollection<ExerciseCategory> Categories
    ...
    public ICollection<Link> Links { get; set; }
}

public class ExerciseCategory
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

public class Link
{
    public string Name { get; set; }
    public string Value { get; set; }
}

I have a view that lets me create this object, but I miss a form control for the property Exercise.Links:

<form id="profile-form" method="post">
<div class="row">
    <div class="col-12 p-sm-2 p-md-3 p-lg-5 bg-custom-white rounded-4">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-floating mt-2 mb-2">
            <input asp-for="Input.Name" class="form-control" />
            <label asp-for="Input.Name" class="form-label"></label>
            <span asp-validation-for="Input.Name" class="text-danger"></span>
        </div>
        <div class="form-floating mt-2 mb-2">
            <input asp-for="Input.Description" class="form-control" />
            <label asp-for="Input.Description" class="form-label"></label>
            <span asp-validation-for="Input.Description" class="text-danger"></span>
        </div>
        <div class="form-floating mt-2 mb-2">
            <select asp-for="Input.CategoryIds" 
                    asp-items="Model.Input.Categories" class="form-control"></select>
            <label asp-for="Input.Categories" class="form-label"></label>
        </div>
        <button id="create-exercise-button" type="submit" class="w-100 btn btn-lg btn-primary">Create</button>
    </div>
</div>
</form>

The property Exercise.Categories is easy as Category is an entity which is created separately, so I just have a dropdown listing the existing categories.

Link is just a value object that is not persisted on its own. Therefore, the user should be able to create/remove/edit Link from the Exercise create/edit page.

I have tried the following:

            @for (var i = 0; i < Model.Input.Links.Count; i++)
            {
                <div>
                    <input asp-for="Input.Links[i].Name">
                    <input asp-for="Input.Links[i].Value">
                </div>
            }

Which results in the following HTML:

<div>
  <input type="text" id="Input_Links_0__Name" name="Input.Links[0].Name" value="some link">
  <input type="text" id="Input_Links_0__Value" name="Input.Links[0].Value" value="https://somelink.com">
</div>
<div>
  <input type="text" id="Input_Links_1__Name" name="Input.Links[1].Name" value="some link1">
  <input type="text" id="Input_Links_1__Value" name="Input.Links[1].Value" value="https://somelink1.com">
</div>

But this doesn't populate the Input.Links property when posting back.

I started thinking that the problem is the fact that my list is a list of Link rather than a list of primitive types. In order to test this, I added a test List<string> to input model called Links1 and tried to populate that list as I did before:

<div class="form-floating mt-2 mb-2">
  <input asp-for="Input.Links1[i]" data-val="true" class="form-control" />
  <label asp-for="Input.Links1[i]" class="form-label"></label>
</div>

This works. I get the populated list back in the controller after submitting the form, which confirms the problem is the List<Link>

How can I add a form control for populating the property Exercise.Link (which is a List<Link>)?


Solution

  • After reading this answer to another question, I figured out what the problem was:

    The class Link, on which the property Exercise.Links depends, has no setter for its Link.Name and Link.Value properties, so the binder cannot populate them when you submit the form.

    This is due to Link being a domain model. The solution in this case was to create a presentation layer model that has public {get; set;} and use that on the form instead of the domain model.

    So, to answer the question properly, this is how you could write a form control to edit a List<Link>:

            @for (var i = 0; i < Model.Input.Links.Count; i++)
            {
                <div>
                    <input asp-for="Input.Links[i].Name">
                    <input asp-for="Input.Links[i].Value">
                </div>
            }