I'm rendering a recursive tree-structure using a ViewComponent, and I'm struggeling with the use of Html.HiddenFor and Html.CheckBoxFor in the form.
This is the ViewModel:
public class ViewModelProductCategory
{
public int Id { get; set; }
public int? ParentId { get; set; }
public string Title { get; set; }
public int SortOrder { get; set; }
public bool Checked { get; set; }
public ViewModelProductCategory ParentCategory { get; set; }
public IEnumerable<ViewModelProductCategory> Children { get; set; }
public IEnumerable<ViewModelProduct> Products { get; set; }
}
The ViewComponent is being invoked from the main View-page like this:
@await Component.InvokeAsync("SelectCategories",
new
{
parentId = 0,
productId = Model.Id // This is the current product Id from the main View
})
... and this is the ViewComponent's Invoke-method:
public async Task<IViewComponentResult> InvokeAsync(int? parentId, int productId)
{
List<ViewModelProductCategory> VM = new List<ViewModelProductCategory>();
if (parentId == 0)
{
VM = _mapper.Map<List<ViewModelProductCategory>>
(await _context.ProductCategories.Include(c => c.Children)
.Where(x => x.ParentId == null).OrderBy(o => o.SortOrder).ToListAsync());
}
else
{
VM = _mapper.Map<List<ViewModelProductCategory>>
(await _context.ProductCategories.Include(c => c.Children)
.Where(x => x.ParentId == parentId).OrderBy(o => o.SortOrder).ToListAsync());
}
foreach (var item in VM)
{
// The Checked-value is not stored in the database, but is set here based
// on the occurances of ProductId in the navigation property Products.Id
// I'm not entirely confident that this statement checks the correct checkboxes...
item.Checked = item.Products.Any(c => c.Id == productId);
}
ViewData["productId"] = productId;
return View(VM);
}
This is the Default.cshtml of the ViewComponent:
@model List<MyStore.Models.ViewModels.ViewModelProductCategory>
<ul style="list-style:none;padding-left:0px;">
@if (Model != null)
{
int ProductId = (ViewData["productId"] != null)
? int.Parse(ViewData["productId"].ToString())
: 0;
@for (int i = 0; i < Model.Count(); i++)
{
<li style="margin-top:4px;padding:0px;">
@if (Model[i].Children.Count() == 0)// Prevent products from being placed in a category with child categories
// by not rendering a checkbox next to it
{
@Html.HiddenFor(model => Model[i].Id)
@Html.CheckBoxFor(model => Model[i].Checked)
}
@Html.LabelFor(model => Model[i].Id, Model[i].Title)
<ul>
@*Let's recurse!*@
@await Component.InvokeAsync("SelectCategories",
new
{
parentId = Model[i].Id,
productId = ProductId
})
</ul>
</li>
}
}
</ul>
Which outputs this HTML (this is a portion of it):
<input id="z0__Id" name="[0].Id" type="hidden" value="1003" />
<input checked="checked" id="z0__Checked" name="[0].Checked" type="checkbox" value="true" />
<label for="z0__Id">Up to 20"</label>
<input data-val="true" data-val-required="The Id field is required." id="z1__Id" name="[1].Id" type="hidden" value="1004" />
<input checked="checked" data-val="true" data-val-required="The Checked field is required." id="z1__Checked" name="[1].Checked" type="checkbox" value="true" />
<label for="z1__Id">21" - 40"</label>
<input data-val="true" data-val-required="The Id field is required." id="z2__Id" name="[2].Id" type="hidden" value="1005" />
<input checked="checked" data-val="true" data-val-required="The Checked field is required." id="z2__Checked" name="[2].Checked" type="checkbox" value="true" />
<label for="z2__Id">41" - 55"</label>
<!-- ...and so on ... -->
I'm faced with (at least) two challenges here:
The statement
item.Products.Any(c => c.Id == productId);should evaluate totrueif the value of the local variableproductIdis found in the navigation propertyProducts, meaning that this particular product is linked to that particular category (viaitem.Products), but now it is checking all sibling categories in the parent-category.I think the HTML-output is not correct, at least for the
name-property:<input checked="checked" id="z0__Checked" name="[0].Checked" type="checkbox" value="true" />. Could it work being named[0].Checked?
I haven't written the controller's POST-edit method yet, so I'm not including it in this question. In it I have to do some converting from one model to another, and some other stuff. If I can just get the form to render correctly, I can bind it to the controller.
Yes, I expect the rendered html names are probably invalid for model binding. You should add a parent model to contain a list of this class:
Then use it in your view and controller:
This will then render in the html: