Ajax queries
This page contains two independent grids similar to the one Grid with query windows example, but
they update with ajax. That's why one may place several grids in the same page: each of them displays
the results of the odata query contained in its private ajax url instead of an unique odata query contained in the
overall page url.
Each grid is rendered by a ViewComponent that takes care of retrieving its data. The same ViewComponent is
called both when the page is rendered and during each ajax query update.
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. |
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. |
Ajax updates are enabled by simply providing an ajaxId
argument when calling the AttachEndpoint
method
on the QueryDescription
object for providing it the query url. The ajaxId argument must contain the Id of the Html
node where the grid Html will be inserted. Give a look to the AttachEndpoint
in the ViewComponent code. To avoid name collisions each grid is rendered by adding a different
Html prefix to all Html nodes ids and names. The prefix and the ajaxId are passed as ViewComponent
arguments. In the ViewComponent code the prefix is attached to the ViewData with:
ViewData.TemplateInfo.HtmlFieldPrefix = prefix;
Then the both ajaxId and prefix are passed also in the url:
query.AttachEndpoint(Url.Action("IndexAjax", "Food", new { prefix = ViewData.TemplateInfo.HtmlFieldPrefix, ajaxId= ajaxId }), ajaxId: ajaxId);
This way, they are received by the IndexAjax
controller action method
that performs the query ajax updates,
and are passed back in future calls to the same ViewComponent
The ajax update has a default "blocking animation" that grays out the grid and blocks any interaction with it. You may change this animation and/or add other effects by providing an endpoint name after the Html node id in the ajaxId parameter. The two strings must be separated by a blank, like in this example: "grid1 myEndpoint".
The grids "back" button is similar to the browser back button but acts only on the grid it is contained in. Each time the user clicks the back button he/she goes back to the previous query. Changing page is not considered a different query so the back button doesnt go back to previous page but directly to the last page visited of the previous query. This way, for instance, user may group data, click a detail button, browse all detail pages, and then with a single click he/she may return to the grouping page, and possibly select a different detail. The grid back button works only when the grid updates with ajax.
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
@{ ViewData["Title"] = "Ajax queries"; } <h2>@ViewData["Title"]</h2> <div id="grid1"> @await Component.InvokeAsync("AjaxGrid", new { prefix = "firstGrid", ajaxId = "grid1" }) </div> <div id="grid2"> @await Component.InvokeAsync("AjaxGrid", new { prefix = "secondGrid", ajaxId = "grid2" }) </div> @section Scripts { <link href="~/lib/awesomplete/awesomplete.css" rel="stylesheet" /> <script src="~/lib/mvcct-controls/mvcct.controls.min.js?v=2"></script> <script src="~/lib/mvcct-controls/modules/mvcct.controls.ajax.min.js?v=2"></script> <script src="~/lib/awesomplete/awesomplete.min.js"></script> <script src="~/lib/mvcct-controls/modules/mvcct.controls.autocomplete.min.js?v=1"></script> <script src="~/lib/mvcct-controls/modules/mvcct.controls.serverGrid.min.js?v=1"></script> <script src="~/lib/mvcct-odata/dest/global/mvcct.odata.min.js?v=2"></script> <script src="~/lib/mvcct-controls/modules/mvcct.controls.query.min.js?v=3"></script> }
using System.Linq; using System.Threading.Tasks; using LiveExamples.Repositories; using LiveExamples.Viemodels; using Microsoft.AspNetCore.Mvc; using MvcControlsToolkit.Core.OData; namespace LiveExamples.ViewComponents { public class AjaxGridViewComponent: ViewComponent { IWebQueryProvider queryProvider; FoodRepository repository; public AjaxGridViewComponent(FoodRepository repository, IWebQueryProvider queryProvider) { this.queryProvider = queryProvider; this.repository = repository; } public async Task<IViewComponentResult> InvokeAsync( string prefix, string ajaxId) { 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>()) }; ViewData.TemplateInfo.HtmlFieldPrefix = prefix; query.AttachEndpoint(Url.Action("IndexAjax", "Food", new { prefix = ViewData.TemplateInfo.HtmlFieldPrefix, ajaxId= ajaxId }), ajaxId: ajaxId); return View(model); } } }
@model LiveExamples.Viemodels.FoodListViewModel <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" /> <query type="Back" /> </toolbar> </grid>
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; } }
using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using MvcControlsToolkit.Controllers; using LiveExamples.Viemodels; using Microsoft.AspNetCore.Mvc; using LiveExamples.Repositories; namespace LiveExamples.Controllers { public class FoodController: ServerCrudController<FoodViewModel, FoodViewModel, int?> { public FoodController(FoodRepository repository, IStringLocalizerFactory factory, IHttpContextAccessor accessor) : base(factory, accessor) { Repository = repository; } public IActionResult IndexAjax(string prefix, string ajaxId) { return ViewComponent("AjaxGrid", new { prefix = prefix, ajaxId = ajaxId }); } public IActionResult GridsContainer() { return View(); } [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 }); }