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 AjaxGridViewComponentViewComponent
    {
        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" />
        &nbsp;
        <query type="Filtering" />
        <query type="Sorting" />
        <query type="Grouping" />
        <query type="Back" />
    </toolbar>
</grid>
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; }
    [Query,
        Display(Name = "supplier")]
    public int SupplierId { getset; }
    [Query,
        Display(Name = "supplier")]
    public string SupplierCompanyName { getset; }
 
}
[RunTimeType]
public class FoodViewModelGroupingFoodViewModel
{
    public int SupplierIdCount { getset; }
    public int PackageCount { getset; }
 
 
}
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<FoodViewModelFoodViewModelint?>
    {
        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 .paginationthead .pagination 
    {
        margin0;
        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))
                .Data;
    }
    static FoodRepository()
    {
        DefaultCRUDRepository<ApplicationDbContextSupplier>
            .DeclareProjection(m => new AutoCompleteItem
            {
                Display = m.CompanyName,
                Value = m.Id
            });
    }

Fork me on GitHub