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
Geitost 500 g 2.50 Norske Meierier
Genen Shouyu 24 - 250 ml bottles 15.50 Mayumi's
Gnocchi di nonna Alice 24 - 250 g pkgs. 38.00 Pasta Buttini s.r.l.
Gorgonzola Telino 12 - 100 g pkgs 12.50 Formaggi Fortini s.r.l.
Grandma's Boysenberry Spread 12 - 8 oz jars 25.00 Grandma Kelly's Homestead
  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.

Important: All information contained in the external-key-remote tag helper on how to render external keys, starting from version 2.1.0 may be specified also with data annotations, thus simplifying the markup and increasing reusability of the code

@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