Detail form display mode/custom columns

This example contains a custom column definition to declare TypeId is an external key whose value is TypeName. Moreover, it handles 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.

color laser printer

400.00

40.00

$

printer

High quality color laser printer

<detail-form asp-for="@Model.SubInfo<ProductViewModelDetail>().Model"
             all-properties="true"
             edit-mode-default="false">
    <column asp-for="TypeId">
        <external-key-static display-property="TypeName" />
    </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> ShowCustom(int? id)
{
    if (!id.HasValue) id = 2;
    var model = await repository
        .GetById<ProductViewModelDetailint>(id.Value);
    return View(model);
}

Fork me on GitHub