Grid with custom rows and query

Important. This is a complex example. Before reading it, please study allgrid and all previousquery examples.

Also complex grids that list different item types in edit mode may use query facilities. However, some cautions must be taken. Namely:

  • All item types have a custom ancesctor, called main item ViewModel whose rendering is usually defined in the first RowType. Query, behavior is defined in the main item ViewModel, since all query may involve just properties common to all item types.
  • In the repository together with the instruction that specifies how to project db data in the various item ViewModels :
    DeclareProjection(
            m => m.Maintenance == null ?
            new ProductViewModel{} :
            new ProductMaintenanceViewModel
            {
                MaintenanceYearlyRate = (decimal)m.Maintenance.YearlyRate
            });
    We must add also an instruction that specifies how to project DB data on the main item ViewModel since it is the one used for querying data:
    DeclareQueryProjection(m => new ProductViewModel { }, m => m.Id);
    
    Where the second argument is an expression that specifies the key property. In fact, LinQ is not able to project queries through a select operation that contains conditions.
 
Name Price Price/Year Cur Type Av
Kurtis 400.00 40.00 printer
laptop 1200.00 computer
@model LiveExamples.Viemodels.ProductlistViewModel
@using LiveExamples.Controllers
@{
    ViewData["Title"] = "grid with custom rows and query";
    if (Model.Query.AttachedTo == null)
    {
        Model.Query.AttachEndpoint(Url.Action("CustomServerQuery""CQGrids"));
    }
}
...
...
...
@*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(CQGridsController)"
          row-id="immediate-custom-query"
          operations="user => Functionalities.FullInLine | Functionalities.GroupDetail"
          query-for="Query"
          sorting-clauses="2"
          enable-query="true"
          query-grouping-type="typeof(ProductViewModel)"
          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"
                  row-id="immediate-custom-query-maintenance"
                  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 class="pagination pagination-sm"
                   max-pages="4"
                   page-size-default="5"
                   total-pages="Products.TotalPages" />
            &nbsp;
            <query type="Filtering" />
            <query type="Sorting" />
            <query type="Grouping" />
            <verify-permission required-permissions="@(Functionalities.Append)">
                <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>
            </verify-permission>
        </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")]
    [Query]
    [Display(Name = "Name", Order = 400)]
    [Required]
    public string Name { getset; }
    [ColumnLayout(DetailWidthsAsString = "60 30")]
    [Query]
    [Display(Name = "Price", Order = 300)]
    public decimal Price { getset; }
    [Query]
    [Display(Name = "Cur", Order = 280)]
    [ColumnLayout(WidthsAsString = "10", DetailWidthsAsString = "40 20")]
    public Currency ChosenCurrency { getset; }
    [Query]
    [ColumnLayout(DetailWidthsAsString = "30")]
    [Display(Name = "Av", Order = 230)]
    public bool Available { getset; }
 
    [Query]
    [Display(Name = "Type", Order = 250)]
    public string TypeName { getset; }
    [Query]
    [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 class CQGridsController : 
    ServerCrudController<ProductViewModelDetailProductViewModelint?>
{
 
    public override string DetailColumnAdjustView { get { return "_DetailRows"; } }
    public IWebQueryProvider queryProvider { getprivate set; }
    public CQGridsController(ProductRepository repository,
        IStringLocalizerFactory factory, IHttpContextAccessor accessor,
        IWebQueryProvider queryProvider)
        : base(factory, accessor)
    {
        Repository = repository;
        this.queryProvider = queryProvider;
    }
        
    [ResponseCache(Duration = 0, NoStore = true)]
    public async Task<IActionResult> CustomServerQuery()
    {
        var query = queryProvider.Parse<ProductViewModel>();
        int pg = (int)query.Page;
        if (pg < 1) pg = 1;
 
        var model = new ProductlistViewModel
        {
            Products = await Repository.GetPage(
            query.GetFilterExpression(),
            query.GetSorting() ??
                (q => q.OrderBy(m => m.Name)),
            pg, 5,
            query.GetGrouping()),
            Query = query
        };
        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;
}
public class ProductRepository : 
    DefaultCRUDRepository<ApplicationDbContextProduct>
{
    private ApplicationDbContext db;
    public ProductRepository(ApplicationDbContext db, IHostingEnvironment hosting)
        : base(db, db.Products){this.db = db; }
    public async Task<IEnumerable<AutoCompleteItem>> GetTypes(string search, int maxpages)
    {
        return (await DefaultCRUDRepository.Create(db, db.ProductTypes)
                .GetPage<AutoCompleteItem>(m => m.Display.StartsWith(search), 
                m => m.OrderBy(n => n.Display), 1, maxpages))
                .Data;
    }
    static ProductRepository()
    {
        DeclareProjection(
                m => m.Maintenance == null ?
                new ProductViewModel{} :
                new ProductMaintenanceViewModel
                {
                    MaintenanceYearlyRate = (decimal)m.Maintenance.YearlyRate
                });
        DeclareQueryProjection(m => new ProductViewModel { }, m => m.Id);
            
        DefaultCRUDRepository<ApplicationDbContextProductType>
            .DeclareProjection(m => new AutoCompleteItem {
                Display=m.Name,
                Value=m.Id
        });
    }
}

Fork me on GitHub