Grid with query windows

This is a read-only grid similar to the one in this simpler example but with querying and paging enabled.

name package type unit price discontinued supplier
Alice Mutton 20 - 1 kg tins 39.00 Pavlova, Ltd.
Aniseed Syrup 12 - 550 ml bottles 10.00 Exotic Liquids
Boston Crab Meat 24 - 4 oz tins 18.40 New England Seafood Cannery
Camembert Pierrot 15 - 300 g rounds 34.00 Gai pâturage
Carnarvon Tigers 16 kg pkg. 62.50 Pavlova, Ltd.
  1. On the controller side the IWebQueryProvider is injected in the controller constructor to get the OData query passed in the Url. IWebQueryProvider Parse method is invoked in each action method to get a QueryDescription object for a specific ViewModel. The query description object is used to get current page, LinQ filter, sorting, and grouping that are passed to the repository GetPage method.
    Please, notice that when the query contains grouping/aggregation a different overload of GetPage is called, that contains a generic argument for the ViewModel to use for the aggregated items.
    The grouping ViewModel MUST inherit from the ViewModel used for the standard item, and adds some more fields to count different occurrences of properties. The name of these count properties, MUST be the name of the property they refer to with the "Count" postfix.
  2. On each ViewModel property querying is enabled and configured with QueryAttribute, FilterLayoutAttribute and with the analogous attributes of the column TagHelper.
  3. QueryDescription object is passed to the View. There, its AttachEndpoint method is invoked to specify the Url where to submit all OData queries.
  4. In the View the QueryDescription object is passed to the grid TagHelper in its query-for attribute. The grid TagHelper contains also enable-query="true" to enable queries, sorting-clauses="2" to specify a maximum of 2 sorting clauses. query-grouping-type specifies the type of the ViewModel used for grouping. Moreover, a row-type TagHelper specifies how to render the grouping item. It ineriths settings from the main row (from-row="0"), and adds just two count properties.
    When grid is rendered in grouping mode only columns involved in current grouping and aggregations operations are shown.
  5. Pager is configured to get page information from the QueryDescription object passed to the grid TagHelper. Therefore, it contains just a few informations, namely: css classes, max number of page links to show, page size, and the property containing the total number of pages. All other information needed by the pager are "inherithed" from the grid TagHelper.
  6. Each query functionality (filtering, sorting and grouping) is added by simply putting the associated query button in a toolbar. Query buttons are generated by the query TagHelper. All information needed by query buttons is inherited by the grid TagHelper, but may be overriden with attributes of the query TagHelper. The Functionalities.GroupDetail in the grid operations attribute shows a group detail button on each grid row when the grid is in grouping mode. By clicking it the grid shows all items that have beem grouped in the row whose button was clicked. Functionalities.GroupDetailNew performs the same operation but shows the detail grid in a new browser tab.
@model LiveExamples.Viemodels.FoodListViewModel
    ViewData["Title"] = "Grid with query";
    if (Model.Query.AttachedTo == null)
<grid  asp-for="Products.Data"
      operations="user => Functionalities.ReadOnly | Functionalities.GroupDetail"
      class="table table-condensed table-bordered" >
        <column asp-for="Products.Data.Element().SupplierId">
            <external-key-remote display-property="Products.Data
                           new { search = "_zzz_" }))"
                max-results="20" />
        <row-type asp-for="Products.Data
                  .SubInfo<FoodViewModelGrouping>().Model" from-row="0">
            <column asp-for="Products.Data
                    .SubElement<FoodViewModelGrouping>().SupplierIdCount" />
            <column asp-for="Products.Data
                    .SubElement<FoodViewModelGrouping>().PackageCount" />
        <toolbar zone-name="@LayoutStandardPlaces.Header">
                   class="pagination pagination-sm"
                   total-pages="Products.TotalPages" />
            <query type="Filtering" />
            <query type="Sorting" />
            <query type="Grouping" />
@section Scripts {
<link href="~/lib/awesomplete/awesomplete.css" rel="stylesheet" />
<script src="~/lib/mvcct-controls/mvcct.controls.min.js"></script>
<script src="~/lib/mvcct-controls/modules/mvcct.controls.ajax.min.js"></script>
<script src="~/lib/awesomplete/awesomplete.min.js"></script>
<script src="~/lib/mvcct-controls/modules/mvcct.controls.autocomplete.min.js"></script>
<script src="~/lib/mvcct-controls/modules/mvcct.controls.serverGrid.min.js"></script>
<script src="~/lib/mvcct-odata/dest/global/mvcct.odata.min.js"></script>
<script src="~/lib/mvcct-controls/modules/mvcct.controls.query.min.js"></script>
public class FoodViewModel
    public int? Id { getset; }
    [QueryStringLength(64, MinimumLength = 2) Required, 
        Display(Name ="name")]
    public string ProductName { getset; }
    [QueryStringLength(32, MinimumLength = 2), Required,
        Display(Name = "package type")]
    public string Package { getset; }
    [Range(0, 1000), Query,
        Display(Name = "unit price")]
    public decimal UnitPrice { getset; }
    [Display(Name = "discontinued")]
    public bool IsDiscontinued { getset; }
        Display(Name = "supplier")]
    public int SupplierId { getset; }
        Display(Name = "supplier")]
    public string SupplierCompanyName { getset; }
public class FoodViewModelGroupingFoodViewModel
    public int SupplierIdCount { getset; }
    public int PackageCount { getset; }
public class FoodControllerServerCrudController<FoodViewModelFoodViewModelint?>
    public FoodController(FoodRepository repository, 
            IStringLocalizerFactory factory, IHttpContextAccessor accessor, 
            IWebQueryProvider queryProvider) :
        base(factory, accessor)
        Repository = repository;
        this.queryProvider = queryProvider;
    public IWebQueryProvider queryProvider { getprivate set; }
    public async Task<IActionResult> Index()
        var query = queryProvider.Parse<FoodViewModel>();
        int pg = (int)query.Page;
        var grouping = query.GetGrouping<FoodViewModelGrouping>();
        var model = new FoodListViewModel
            Query = query,
            Products =
                grouping == null ?
                    await Repository.GetPage(
                        query.GetSorting() ??
                            (q => q.OrderBy(m => m.ProductName)),
                        pg, 5)
                    await Repository.GetPageExtended(
                        query.GetSorting<FoodViewModelGrouping>() ??
                            (q => q.OrderBy(m => m.ProductName)),
                        pg, 5,
        return View(model);
    public async Task<ActionResult> GetSuppliers(string search)
        var res = search == null || search.Length < 3 ?
            new List<AutoCompleteItem>() :
            await (Repository as FoodRepository).GetSuppliers(search, 10);
        return Json(res);
tfoot .paginationthead .pagination 
        displayinline !important;
public class FoodRepository : DefaultCRUDRepository<ApplicationDbContextFood>
    private ApplicationDbContext db;
    public  FoodRepository(ApplicationDbContext db):
        base(db, db.Foods)
        this.db = db;
    public async Task<IEnumerable<AutoCompleteItem>> 
        GetSuppliers(string search, int maxpages)
        return (await DefaultCRUDRepository.Create(db, db.Suppliers)
                .GetPage<AutoCompleteItem>(m => m.Display.StartsWith(search),
                m => m.OrderBy(n => n.Display), 1, maxpages))
    static FoodRepository()
            .DeclareProjection(m => new AutoCompleteItem
                Display = m.CompanyName,
                Value = m.Id

Fork me on GitHub