Immediate grid with custom columns/rows

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) to declare Price fas a colspan=2. This examples handles also the possibility that some item models 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 colspan is overriden to 1 so that both fields fit into the Price column of the father class. Column and row definitions may provide also custom edit/display templates defined in-line, with partial views and with ViewComponents. Each control gives also the possibility to redefine its overall layout template. Thus, for instance, the grid control may be easily turned into an ul/li list while keeping entirely its functionalities. Please refer to this detail form example to see an in-line custom template definition. 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.
Name Price Cur Type Av
Name Price Price/Year Cur Type Av
16G Tablet 400.00 $ computer
1T High speed 120.00 disk
1T SSD 200.00 5.00 ssd disk
3D Glasses 400.00 $ display
color laser printer 400.00 40.00 $ printer
@*antiforgery is compulsory *@
<form asp-antiforgery="true">
    <div asp-validation-summary="All" class="text-danger"></div>
    <grid asp-for="Products.Data"
          type="Immediate"
          all-properties="true"
          mvc-controller="typeof(CGridsController)"
          row-id="immediate-custom-example"
          operations="user => Functionalities.FullInLine"
          class="table table-condensed table-bordered">
        <column asp-for="Products.Data.Element().TypeId">
            <external-key-remote display-property="Products.Data.Element().TypeName"
                                 items-value-property="Value"
                                 items-display-property="Display"
                                 items-url="@(Url.Action("GetTypes""CGrids", 
                                            new { search = "_zzz_" }))"
                                 dataset-name="product-types"
                                 url-token="_zzz_"
                                 max-results="20" />
        </column> />
        <column asp-for="Products.Data.Element().Price" colspan="2" />
        <row-type asp-for="Products.Data.SubInfo<ProductMaintenanceViewModel>().Model"
                  from-row="0">
            <column asp-for="Products.Data.Element().Price" colspan="1" />
            <column asp-for="Products.Data.SubElement<ProductMaintenanceViewModel>()
                                .MaintenanceYearlyRate" />
        </row-type>
        <toolbar zone-name="@LayoutStandardPlaces.Header">
            <pager mode="CustomPages"
                   class="pagination pagination-sm"
                   max-pages="4"
                   current-page="Products.Page"
                   page-size-default="5"
                   total-pages="Products.TotalPages"
                   skip-url-token="_zzz_"
                   url-default="@Url.Action("CustomServer",
                               "CGrids"new {page="_zzz_" })" />
            <button type="button" data-operation="add append 0"
                    class="btn btn-sm btn-primary">
                new product
            </button>
            <button type="button" data-operation="add append 1"
                    class="btn btn-sm btn-primary">
                new product with maintenance
            </button>
        </toolbar>
    </grid>
</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> CustomServer(int? page)
{
    int pg = page.HasValue ? page.Value : 1;
    if (pg < 1) pg = 1;
 
    var model = new ProductlistViewModel
    {
        Products = await Repository.GetPage<ProductViewModel>(
        null,
        q => q.OrderBy(m => m.Name),
        pg, 5)
    };
    return View(model);
}
 
[HttpGet]
public async Task<ActionResult> GetTypes(string search)
{
    var res = search == null || search.Length < 3 ?
        new List<AutoCompleteItem>() :
        await (Repository as ProductRepository).GetTypes(search, 10);
    return Json(res);
}
tfoot .paginationthead .pagination
 {
    margin0;
    displayinline !important;
}
 
.full-cell {
    width100%;
}
 
table .input-validation-error {
    border1px solid red;
    background-colormistyrose;
}

Fork me on GitHub