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>
)?
After reading this answer to another question, I figured out what the problem was:
The class
Link
, on which the propertyExercise.Links
depends, has no setter for itsLink.Name
andLink.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>
}