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. |
-
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. - On each ViewModel property querying is enabled and configured with QueryAttribute, FilterLayoutAttribute and with the analogous attributes of the column TagHelper.
- QueryDescription object is passed to the View. There, its AttachEndpoint method is invoked to specify the Url where to submit all OData queries.
-
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. - 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.
- 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) { Model.Query.AttachEndpoint(Url.Action("Index", "Food")); } } ... ... ... <grid asp-for="Products.Data" type="Immediate" all-properties="true" mvc-controller="typeof(LiveExamples.Controllers.GridsController)" row-id="readonly-query" operations="user => Functionalities.ReadOnly | Functionalities.GroupDetail" query-for="Query" sorting-clauses="2" enable-query="true" query-grouping-type="typeof(FoodViewModelGrouping)" class="table table-condensed table-bordered" > <column asp-for="Products.Data.Element().SupplierId"> <external-key-remote display-property="Products.Data .Element().SupplierCompanyName" items-value-property="Value" items-display-property="Display" items-url="@(Url.Action("GetSuppliers", "Food", new { search = "_zzz_" }))" dataset-name="suppliers" url-token="_zzz_" max-results="20" /> </column> <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" /> </row-type> <toolbar zone-name="@LayoutStandardPlaces.Header"> <pager class="pagination pagination-sm" max-pages="4" page-size-default="5" total-pages="Products.TotalPages" /> <query type="Filtering" /> <query type="Sorting" /> <query type="Grouping" /> </toolbar> </grid>...
...
...
@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 { get; set; } [Query, StringLength(64, MinimumLength = 2) Required, Display(Name ="name")] public string ProductName { get; set; } [Query, StringLength(32, MinimumLength = 2), Required, Display(Name = "package type")] public string Package { get; set; } [Range(0, 1000), Query, Display(Name = "unit price")] public decimal UnitPrice { get; set; } [Display(Name = "discontinued")] public bool IsDiscontinued { get; set; } [Query, Display(Name = "supplier")] public int SupplierId { get; set; } [Query, Display(Name = "supplier")] public string SupplierCompanyName { get; set; } } [RunTimeType] public class FoodViewModelGrouping: FoodViewModel { public int SupplierIdCount { get; set; } public int PackageCount { get; set; } }
public class FoodController: ServerCrudController<FoodViewModel, FoodViewModel, int?> { public FoodController(FoodRepository repository, IStringLocalizerFactory factory, IHttpContextAccessor accessor, IWebQueryProvider queryProvider) : base(factory, accessor) { Repository = repository; this.queryProvider = queryProvider; } public IWebQueryProvider queryProvider { get; private 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.GetFilterExpression(), query.GetSorting() ?? (q => q.OrderBy(m => m.ProductName)), pg, 5) : await Repository.GetPageExtended( query.GetFilterExpression(), query.GetSorting<FoodViewModelGrouping>() ?? (q => q.OrderBy(m => m.ProductName)), pg, 5, query.GetGrouping<FoodViewModelGrouping>()) }; return View(model); } [HttpGet] 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 .pagination, thead .pagination { margin: 0; display: inline !important; }
public class FoodRepository : DefaultCRUDRepository<ApplicationDbContext, Food> { 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)) .Data; } static FoodRepository() { DefaultCRUDRepository<ApplicationDbContext, Supplier> .DeclareProjection(m => new AutoCompleteItem { Display = m.CompanyName, Value = m.Id }); }