Detail form edit mode/custom columns

This example contains 2 custom column definitions: 1) to declare TypeId is an external key whose value is TypeName, and whose values must be provided by an autocomplete. 2) A custom column edit template to edit the Description property in a text area. Custom edit templates may be provided in-line, with partial views and with ViewComponents. One may provide also row custom templates. Custom templates may be installed also globally by re-defining the partial views with the default templates either in a controller specific folder or in the shared folder. This examples handles also the possibility that the actual model be a subclass containing futher properties. For this reason a second row-type is defined whose type matches the subclass type, and whose column definitions are inherited from the 0 index main row-type. Then, the new subclass specific MaintenanceYearlyRate column is added and the Price column layout settings are overriden to make room for the new column.

<detail-form asp-for="@Model.SubInfo<ProductViewModelDetail>().Model"
             all-properties="true"
             edit-mode-default="true"
             form-method="POST"
             form-action="@Url.Action("EditCustom""DetailForm")">
    <column asp-for="TypeId">
        <external-key-remote display-property="TypeName"
                             items-value-property="Value"
                             items-display-property="Display"
                             items-url="@(Url.Action("GetTypes""DetailForm", 
                                        new { search = "_zzz_" }))"
                             dataset-name="product-types"
                             url-token="_zzz_"
                             max-results="20" />
    </column>
    <column asp-for="Description">
        <asp-template type="Edit">
            @{ {
                    var Model = Html.Item<string>();
                    <div class="form-group col-xs-12">
                        <label asp-for="@Model"></label>
                        <textarea class="form-control" asp-for="@Model"></textarea>
                        <span asp-validation-for="@Model" class="text-danger"></span>
 
                    </div>
                } }
        </asp-template>
 
    </column>
    <row-type asp-for="@Model.SubInfo<ProductMaintenanceViewModelDetail>().Model" from-row="0">
        <column asp-for="Price" detail-widths="new decimal[] {30, 15 }" />
        <column asp-for="@((Model as ProductMaintenanceViewModelDetail).MaintenanceYearlyRate)" />
    </row-type>
</detail-form>
     /*
DetailWidthsAsString in ColumnLayout specifies percentage width 
for different device screen widths, while WidthsAsString
percentage width when shown in-line within a collection control.
Oder specify the order columns are rendered. Higher Order come first.
All setting, included Display/Name may be overriden by providing
a column definition TagHelper within the control definition.
 */
public class ProductViewModel 
{
    public int? Id { getset; }
    [MaxLength(128)]
    [ColumnLayout(DetailWidthsAsString = "100 50")]
    [Display(Name = "Name", Order = 400)]
    [Required]
    public string Name { getset; }
    [ColumnLayout(DetailWidthsAsString = "60 30")]
    [Display(Name = "Price", Order = 300)]
    public decimal Price { getset; }
    [Display(Name = "Cur", Order = 280)]
    [ColumnLayout(WidthsAsString = "10", DetailWidthsAsString = "40 20")]
    public Currency ChosenCurrency { getset; }
    [ColumnLayout(DetailWidthsAsString = "30")]
    [Display(Name = "Av", Order = 230)]
    public bool Available { getset; }
 
    [Display(Name = "Type", Order = 250)]
    public string TypeName { getset; }
    [Display(Name = "Type", Order = 250)]
    [ColumnLayout(DetailWidthsAsString = "70")]
    public int? TypeId { getset; }
 
}   
/*
 All subclasses whose exact type is inferred at run-time by
 model binders/JSON deserializer must be decorated with the
 RunTimeType to prevent attacks consisting in the forced creation
 of dangerous types
     */
[RunTimeType]
public class ProductMaintenanceViewModel: 
    ProductViewModelIUpdateConnections
{
    [Display(Name = "Price/Year", Order = 299)]
    
    public decimal MaintenanceYearlyRate { getset; }
    // IUpdateConnections specifies when to 
    //update a connected entity contained in a property
    //with name prefix in the DB model  
    public bool MayUpdate(string prefix)
    {
        return prefix == "Maintenance";
    }
}
 
public class ProductViewModelDetailProductViewModel
{
    [MaxLength(256)]
    [Display(Name = "Description", Order = 100)]
    [DisplayFormat(NullDisplayText = "no description available")]
    public string Description { getset; }
}
/*
All subclasses whose exact type is inferred at run-time by
model binders/JSON deserializer must be decorated with the
RunTimeType to prevent attacks consisting in the forced creation
of dangerous types
    */
[RunTimeType]
public class ProductMaintenanceViewModelDetail : 
    ProductViewModelDetailIUpdateConnections
{
    [Display(Name = "Price/Year", Order = 299)]
    [ColumnLayout(DetailWidthsAsString = "30 15")]
    public decimal MaintenanceYearlyRate { getset; }
    // IUpdateConnections specifies when to 
    //update a connected entity contained in a property
    //with name prefix in the DB model  
    public bool MayUpdate(string prefix)
    {
        return prefix == "Maintenance";
    }
}
public async Task<IActionResult> EditCustom(int? id)
{
    if (!id.HasValue) id = 4;
    var model = await repository
        .GetById<ProductViewModelDetailint>(id.Value);
    return View(model);
}
[HttpPost]
public async Task<ActionResult> EditCustom(ProductViewModelDetail model)
{
 
    if (ModelState.IsValid)
    {
        repository.Update(false, model);
        await repository.SaveChanges();
        return RedirectToAction("EditCustom"new { id = model.Id });
    }
    return View(model);
}

Fork me on GitHub