Adjusting Grid Column with Resolution with Paging and Sorting

We have to show information inside a grid. I first used the WebMatrix Web Grid that allow to have sorting, paging, ajax call back etc. The problem is that it does not work very well if you want something flexible. I also had problem with paging and with sorting. At the end, I figured it out that it was less problem to just create an Html grid and using some custom Html Helper to create something very clean. I also wanted to integrate the FootTable.js JavaScript library to have dynamic column display. It was very hard with the WebMatrix and a breeze with custom html helper. An example is that I wanted to sort on a property that when clicked and sent to the controller was the name of the view model. The problem was that the WebMatrix must have the same name between view model and model otherwise the mapping cannot be done. This article explains how to create a grid that can be sort-able with paging and that when re-sizeable continue to be usable.

Here is images of the final result. The first image is when the resolution full screen (desktop).

This one is for tablet resolution:

This one is for phone. The second row is open to show you that the FooTable.js library allows you to have some interesting features.

I will not discuss here about the filter capability because it is a subject that does not touche the grid. This allow to have some condition on the data set that is bound to the grid but does not affect the creation of the grid.

View Model

The first thing is that I was using WebMatrix which required to have some field like “sort”, “sortdir” and “page” defined in lowercase. This is why you will see in few that I have still used this nomenclature. Now, it could be renamed but I have not do it yet. Nevertheless, the principle is the same. You need to have your view model, that has the list of element you want to bind to the grid, to inherit from a base class that has these three properties. These one are used to know the sort property, the sort direction and which page to load information.

public interface IWebGridPagingInformation
{
    string sort { get; set; }
    string sortdir { get; set; }
    int page { get; set; }
}
 
public class GridFilterSortableViewModel:WebPageViewModel, IWebGridPagingInformation
{
    public string sort { get; set; }
    public string sortdir { get; set; }
    public int page { get; set; }
        public bool HasSortAndPagingFilter
        {
            get { return !(sort == null && sortdir == null && page == default(int)); }
        }
}

The Interface is used for information about the sort and paging. It will be mapped later to a interface that the model known to execute its query with Entity Framework.

This last class, GridFilterSortableViewModel, can be used for your concrete implementation for your entity. For example, I have a list of Contest entity. This class has the list of contest to display and a property that is all filters that you want to apply to the entity. This allow to offer more filter or less than what it is displayed.

public class ContestListViewModel : Infrastructures.GridFilterSortableViewModel
{
    public ContestListViewModel()
    {
        Filters = new ContestListFilterFieldsViewModel();
    }
 
    public IEnumerable<ContestListItemViewModel> Contests { get; set; }
    public ContestListFilterFieldsViewModel Filters { get; set; }
 
}

On the controller side, one action is available. Everything is within GET which allow to use filtering and paging from an URL. This is very interesting to communicate a grid preference by URL.

[HttpGet]
[Authorize(Roles = ApplicationRole.ADMINISTRATOR_MODERATOR)]
public ActionResult List(ContestListViewModel contestListViewModel)
{
    //Get filter contests
    var contestsListModel = this.MapperFacade.GetModel<ContestList, ContestListViewModel>(contestListViewModel);
    var contests = this.contestService.GetContestsByFilter(contestsListModel.Filters);//This one load the filter with good value (COUNT of contest) + return contest for filter
    contestsListModel.Contests = contests;
 
    //Transform back everything for the page
    var viewModelWithContestsAndFilter = this.MapperFacade.GetViewModel<ContestList, ContestListViewModel>(contestsListModel);
 
    //View Model
    viewModelWithContestsAndFilter.InitializeTitles();
    ModelState.Clear();
 
    //Return only a partial if the grid has called the controller with Ajax call
    return View(viewModelWithContestsAndFilter);
}

The first step in the action is to convert the contest list view model into the model. The model is simple and allows to have columns in the sort and filters to be translated into the model property name and not anymore the view model property name.

public class ContestList
{
    public IEnumerable<Contest> Contests { get; set; }
    public ContestListFilterFields Filters { get; set; }
}

The ContestListFilterFields has all Contest Filter fields that we allow to filter but also inherit from FilterEntity.

public class ContestListFilterFields : FilterEntity
{
 
    public LocalizedString Name { get; set; }
 
    public DateTime? StartingTime { get; set; }
    public DateTime? EndingTime { get; set; }
 
    [Display(ResourceType = typeof(ModelPropertyDisplayName), Name = "IsUsingStockRules")]
    public bool IsUsingStockRules { get; set; }
    [Display(ResourceType = typeof(ModelPropertyDisplayName), Name = "IsUsingShortRules")]
    public bool IsUsingShortRules { get; set; }
    [Display(ResourceType = typeof(ModelPropertyDisplayName), Name = "IsUsingOptionRules")]
    public bool IsUsingOptionRules { get; set; }
 
    public Money InitialCapital { get; set; }
 
    public bool IsFilteringOnInitialCapital
    {
        get { return InitialCapital != null; }
    }
 
    public ContestListFilterFields()
    {
        Sort = CrossLayer.Reflections.LambdaHelper.GetPropertyName<ContestListFilterFields>(d => d.StartingTime);
        SortDirection = SortDirection.Desc;
    }
}

The constructor define the default behavior of the grid. As you can see, it is possible to use the sort property to set the default to be the starting time in descending order. The mapping will set these default properties to model.

Mapper.CreateMap<ContestListFilterFields, ContestListFilterFieldsViewModel>()
    .ForMember(d=>d.EndingTime, option=>option.MapFrom(d=>d.EndingTime))
    .ForMember(d => d.InitialCapital, option => option.MapFrom(d => d.InitialCapital ?? new Money()))
    .ForMember(d=>d.IsUsingOptionRules, option=>option.MapFrom(d=>d.IsUsingOptionRules))
    .ForMember(d=>d.IsUsingShortRules, option=>option.MapFrom(d=>d.IsUsingShortRules))
    .ForMember(d=>d.IsUsingStockRules, option=>option.MapFrom(d=>d.IsUsingStockRules))
    .ForMember(d=>d.Name, option=>option.MapFrom(d=>d.Name))
    .ForMember(d=>d.RowsPerPage, option=>option.MapFrom(d=>d.NumberOfEntitiesPerRequest))
    .ForMember(d => d.StartingTime, option => option.MapFrom(d => d.StartingTime))
    .ForMember(d => d.TotalRecords, option => option.MapFrom(d => d.EntityTotalCount))
    ;
Mapper.CreateMap<ContestListFilterFieldsViewModel, ContestListFilterFields>()
    .ForMember(d => d.EndingTime, option => option.MapFrom(d => d.EndingTime))
    .ForMember(d => d.InitialCapital, option => option.MapFrom(d => d.InitialCapital.Value!=0?d.InitialCapital:null))
    .ForMember(d => d.IsUsingOptionRules, option => option.MapFrom(d => d.IsUsingOptionRules))
    .ForMember(d => d.IsUsingShortRules, option => option.MapFrom(d => d.IsUsingShortRules))
    .ForMember(d => d.IsUsingStockRules, option => option.MapFrom(d => d.IsUsingStockRules))
    .ForMember(d => d.Name, option => option.MapFrom(d => d.Name))
    .ForMember(d => d.StartingTime, option => option.MapFrom(d => d.StartingTime))
    ;

The FilterEntity class that inherit your concrete ContestListFilterFields is very generic which mean that you have all what you have for all future entities. It has the current page property, the property name for the sort, the sort direction, total count of entities and the number of entity to display per page.

public class FilterEntity:IPagingInformation
{
    public const int DEFAULT_CURRENT_PAGE = 0;
    public const int NUMBER_OF_ENTITIES_PER_PAGE = 3;
 
 
    public FilterEntity()
    {
        SetDefaultValues();
    }
    public int NumberOfEntitiesPerRequest { get; set; }
    public int CurrentPage { get; set; }
    public string Sort { get; set; }
    public SortDirection SortDirection { get; set; }
    public int EntityTotalCount { get; set; }
 
    public void SetDefaultValues()
    {
        this.CurrentPage = DEFAULT_CURRENT_PAGE;
        this.NumberOfEntitiesPerRequest = NUMBER_OF_ENTITIES_PER_PAGE;
        this.SortDirection = SortDirection.Asc;
 
    }
}

The last thing to do is to mark the table with the sort direction. It does two things. The first is to set the arrow for the current view (with the current field that could have been set by URL or by form). The second thing is to apply the FootTable plugin to the table.

$(document).ready(function () {
    setArrows('contests-list');
    $('#contests-list table').footable();
});
 
/*
 * @description: Set the arrows to the column header that is active for sorting
 */
function setArrows(idDivisionThatContainTable) {
    var dir = $('#' + Application.ControldsId.SortId).val().toUpperCase();
    var col = $('#' + Application.ControldsId.ColumnId).val();
    var header = $('#'+idDivisionThatContainTable+' th a[href*=' + col + ']');
    if (dir === 'ASC') {
        header.text(header.text() + ' ▲');
    }
    if (dir === 'DESC') {
        header.text(header.text() + ' ▼');
    }
};

As you can see, the method use a Aplication.ControldsId.SortID and ColumnId that is set inside a script tag of the view.

@section scripts
{
    <script>
        Application.ControldsId.SortId = "@Html.IdFor(d=>d.sortdir)";
        Application.ControldsId.ColumnId = "@Html.IdFor(d=>d.sort)";
    </script>
    <script src="~/Scripts/Customs/Views/ContestList.js"></script>
}

This could be improved because in a situation of two tables in a page it will not work. However, it would not be the only thing that would not work. The URL parameter is only designed for one grid per page. Nevertheless, this is almost always the case.

More about Patrick Desjardins

Leave a Reply

Your email address will not be published. Required fields are marked *