Serenity: [Tutorial] Custom grid & grid pager without counting total records

Created on 24 May 2019  路  15Comments  路  Source: serenity-is/Serenity

Relate to some issues at here https://github.com/volkanceylan/Serenity/issues/4250 , https://github.com/volkanceylan/Serenity/issues/4399

I created a post to demo how to create a custom grid which just has only next/previous buttons, it will not count total records so in some cases (ex, table with many records) we won't worry about timeout issue

Here it is: Grid Mixin Custom grid pager without counting total records

Screenshot
image

Most helpful comment

Hi everyone, here is new code, at this version we implement it as a "mixin"

btw, I am waiting this pull request https://github.com/volkanceylan/Serenity/pull/4474, if that PR is approved then we can use CustomData and we don't need to create MyBaseListRequest, we also don't need to update parameter for Endpoint/Repository List method, therefore we will write less code


MyBaseListRequest.cs

public interface IPagingRequest
{
    bool EnableOnlyNextPreviousMode { get; set; }
}

public class MyBaseListRequest : ListRequest, IPagingRequest
{
    public bool EnableOnlyNextPreviousMode { get; set; }
}

CustomPagerWithOnlyNextPreviousMixin.ts

namespace [YOUR_NAME_SPACE].Common {
    export class CustomPagerWithOnlyNextPreviousMixin<TItem> {

        private options: CustomPagerWithOnlyNextPreviousMixinOptions<TItem>;
        private dataGrid: Serenity.DataGrid<TItem, any>;

        private _customPagerCurrentPage: number = 1;
        private _customPager: JQuery = $("<span class='next-previous-pager'><button class='custompager-pre'><strong>芦</strong> Previous</button><span style='padding: 0 2px;'></span><button class='custompager-next'>Next <strong>禄</strong></button><span style='padding: 0 2px;'></span><b>Page</b> <span class='custompager-curpage'>1</span></span>");
        private _originalPager = $(".s-SlickPager");
        private _pagingMode: ('full' | 'next-previous-only');
        private _btnSwitch: JQuery;

        constructor(options: CustomPagerWithOnlyNextPreviousMixinOptions<TItem>) {

            var self = this;
            this.options = options;
            var dg = this.dataGrid = options.grid;
            this._pagingMode = options.pagingMode = options.pagingMode || 'next-previous-only';
            $(".slick-pg-in").hide();

            this._originalPager.find(".slick-pg-in").append(this._customPager);

            var btnSwitch = this._btnSwitch = $('<input type="checkbox" title="Full Pager" class="paging-mode-switch pull-right" style="margin-right: 5px" ' + (options.pagingMode == "full" ? ' checked' : '') + '/>')
                .appendTo(dg.element.find(".slick-pg-in"));

            btnSwitch.change((evt) => {

                var isFullMode: boolean = $(evt.target).is(":checked");

                // update current page number
                if (!isFullMode) {
                    this._customPagerCurrentPage = parseInt($(".slick-pg-current").val());
                    this._originalPager.find(".custompager-curpage").text($(".slick-pg-current").val());
                }

                this.switchView(isFullMode ? 'full' : 'next-previous-only');
            });

            this._originalPager.find(".custompager-pre").click(e => {
                if (this._customPagerCurrentPage > 1) {
                    this._customPagerCurrentPage--;
                    this.dataGrid.view.seekToPage = this._customPagerCurrentPage;
                    this.dataGrid.refresh();
                    this._originalPager.find(".custompager-curpage").text(this._customPagerCurrentPage);
                }
                return;
            });

            this._originalPager.find(".custompager-next").click(e => {
                this._customPagerCurrentPage++;
                this.dataGrid.view.seekToPage = this._customPagerCurrentPage;
                this.dataGrid.refresh();
                this._originalPager.find(".custompager-curpage").text(this._customPagerCurrentPage);
                return;
            });

            dg.view.onDataChanged.subscribe(() => {
                this.updatePageControls(!$(this._btnSwitch).is(":checked"));
            });

            // save setting
            var oldCurrentSettings = (dg as any).getCurrentSettings;

            (dg as any).getCurrentSettings = function (flag) {
                var settings = oldCurrentSettings.apply(dg, [flag]);
                settings['customPagerMode'] = $(btnSwitch).is(":checked") ? 'full' : 'next-previous-only';

                return settings;
            };

            var oldRestoreSettings = (dg as any).restoreSettings;

            (dg as any).restoreSettings = function (settings, flags) {
                oldRestoreSettings.apply(dg, [settings, flags]);
                if (settings == null) {
                    var storage = this.getPersistanceStorage();
                    if (storage == null) {
                        self.switchView(self._pagingMode);
                        return;
                    }
                    var json = Q.trimToNull(storage.getItem(this.getPersistanceKey()));
                    if (!json) {
                        self.switchView(self._pagingMode);
                        return;
                    }
                    settings = JSON.parse(json);
                }


                var viewPagerMode = settings.customPagerMode || self._pagingMode;
                var currentViewPagerMode = $(btnSwitch).is(":checked") ? 'full' : 'next-previous-only';

                if (viewPagerMode != currentViewPagerMode) {
                    $(btnSwitch).click();
                }
            };
        }

        public updateNextButton(nbrOfRecords: number, nbrOfRowsPerPage: number): void {
            if (this.options.pagingMode === 'full') {
                return;
            }

            if (nbrOfRecords == 0 || nbrOfRecords < nbrOfRowsPerPage) {
                this._originalPager.find(".custompager-next").prop("disabled", true);
                this._originalPager.find(".custompager-next").css("opacity", 0.5);
            }
            else {
                this._originalPager.find(".custompager-next").prop("disabled", false);
                this._originalPager.find(".custompager-next").css("opacity", 1);
            }
        }

        private switchView(pMode: ('full' | 'next-previous-only')): void {
            this.updatePageControls(pMode == "next-previous-only");
            this.dataGrid.refresh();
            (this.dataGrid as any).persistSettings();
        }

        private updatePageControls(isNextPreviousOnlyMode: boolean) {
            if (isNextPreviousOnlyMode) {
                this._originalPager.find(".next-previous-pager").show();
                this._originalPager.find(".slick-pg-grp").hide();
                this._originalPager.find(".slick-pg-sep").hide();
                this._originalPager.find(".slick-pg-grp:first").show();
            }
            else {
                this._originalPager.find(".next-previous-pager").hide();
                this._originalPager.find(".slick-pg-grp").show();
                this._originalPager.find(".slick-pg-sep").show();
            }

            $(".slick-pg-in").show();
        }

        public getCurrentPagerMode(): ('full' | 'next-previous-only') {
            return $(this._btnSwitch).is(":checked") ? 'full' : 'next-previous-only';
        }
    }

    export class CustomPagerWithOnlyNextPreviousMixinOptions<TItem> {
        grid: Serenity.DataGrid<TItem, any>;
        rowPerPage: number;
        pagingMode?: ('full' | 'next-previous-only');
    }
}

Endpoint.cs

public ListResponse<MyRow> List(IDbConnection connection, MyBaseListRequest request)
{
    return new MyRepository().List(connection, request);
}

Repository.cs

public ListResponse<MyRow> List(IDbConnection connection, MyBaseListRequest request)
{
    return new MyListHandler().Process(connection, request);
}

public class CustomListRequestHandle<TRow> : ListRequestHandler<TRow> where TRow : Row, new()
{
    protected override void ApplyFilters(SqlQuery query)
    {
        base.ApplyFilters(query);

        if (Request is MyBaseListRequest customRequest)
        {
            if (customRequest.EnableOnlyNextPreviousMode)
            {
                query.ApplySkipTakeAndCount(this.Request.Skip, this.Request.Take, this.Request.ExcludeTotalCount || DistinctFields != null);

                // Setting CountRecords to false stops the count(*) query from running
                query.CountRecords = false;
            }
        }                
    }
}

Grid.ts

/// <reference path="../../common/mixin/custompagerwithonlynextpreviousmixin.ts" />

private _pagerMixin: Common.CustomPagerWithOnlyNextPreviousMixin<Your_Row>;

protected onViewProcessData(response: Serenity.ListResponse<Your_Row>): Serenity.ListResponse<Your_Row> {
    var lr = super.onViewProcessData(response);

    this._pagerMixin.updateNextButton(lr.Entities.length, response.Take);

    return lr;
}

protected getViewOptions() {
    var opt = super.getViewOptions();
    opt.rowsPerPage = 20;
    return opt;
}

protected createToolbarExtensions(): void {
    super.createToolbarExtensions();
    var self = this;

    this._pagerMixin = new Rydell.Web.Common.CustomPagerWithOnlyNextPreviousMixin({
        grid: this,
        rowPerPage: this.getPagerOptions().rowsPerPage
    });
}

protected onViewSubmit() {
    if (!super.onViewSubmit()) {
        return false;
    }
    var request = this.view.params as MyBaseListRequest;
    request.EnableOnlyNextPreviousMode = this._pagerMixin.getCurrentPagerMode() == 'next-previous-only';
    return true;
}

protected getPersistanceStorage(): Serenity.SettingStorage {
    return new Common.UserPreferenceStorage();
}

Feel free to ask any question 馃挅

All 15 comments

Thanks @minhhungit, this is really useful!

I'm also interested to see how/where you are tracking user actions?

@edwardch it's not complex, just put your "insert log" code into your methods, it will insert new record into a "action log" table, the rest, serenity will generate form for you

in next days I will update this tutorial, here is trailer for the updating

trailer

Hi everyone, here is new code, at this version we implement it as a "mixin"

btw, I am waiting this pull request https://github.com/volkanceylan/Serenity/pull/4474, if that PR is approved then we can use CustomData and we don't need to create MyBaseListRequest, we also don't need to update parameter for Endpoint/Repository List method, therefore we will write less code


MyBaseListRequest.cs

public interface IPagingRequest
{
    bool EnableOnlyNextPreviousMode { get; set; }
}

public class MyBaseListRequest : ListRequest, IPagingRequest
{
    public bool EnableOnlyNextPreviousMode { get; set; }
}

CustomPagerWithOnlyNextPreviousMixin.ts

namespace [YOUR_NAME_SPACE].Common {
    export class CustomPagerWithOnlyNextPreviousMixin<TItem> {

        private options: CustomPagerWithOnlyNextPreviousMixinOptions<TItem>;
        private dataGrid: Serenity.DataGrid<TItem, any>;

        private _customPagerCurrentPage: number = 1;
        private _customPager: JQuery = $("<span class='next-previous-pager'><button class='custompager-pre'><strong>芦</strong> Previous</button><span style='padding: 0 2px;'></span><button class='custompager-next'>Next <strong>禄</strong></button><span style='padding: 0 2px;'></span><b>Page</b> <span class='custompager-curpage'>1</span></span>");
        private _originalPager = $(".s-SlickPager");
        private _pagingMode: ('full' | 'next-previous-only');
        private _btnSwitch: JQuery;

        constructor(options: CustomPagerWithOnlyNextPreviousMixinOptions<TItem>) {

            var self = this;
            this.options = options;
            var dg = this.dataGrid = options.grid;
            this._pagingMode = options.pagingMode = options.pagingMode || 'next-previous-only';
            $(".slick-pg-in").hide();

            this._originalPager.find(".slick-pg-in").append(this._customPager);

            var btnSwitch = this._btnSwitch = $('<input type="checkbox" title="Full Pager" class="paging-mode-switch pull-right" style="margin-right: 5px" ' + (options.pagingMode == "full" ? ' checked' : '') + '/>')
                .appendTo(dg.element.find(".slick-pg-in"));

            btnSwitch.change((evt) => {

                var isFullMode: boolean = $(evt.target).is(":checked");

                // update current page number
                if (!isFullMode) {
                    this._customPagerCurrentPage = parseInt($(".slick-pg-current").val());
                    this._originalPager.find(".custompager-curpage").text($(".slick-pg-current").val());
                }

                this.switchView(isFullMode ? 'full' : 'next-previous-only');
            });

            this._originalPager.find(".custompager-pre").click(e => {
                if (this._customPagerCurrentPage > 1) {
                    this._customPagerCurrentPage--;
                    this.dataGrid.view.seekToPage = this._customPagerCurrentPage;
                    this.dataGrid.refresh();
                    this._originalPager.find(".custompager-curpage").text(this._customPagerCurrentPage);
                }
                return;
            });

            this._originalPager.find(".custompager-next").click(e => {
                this._customPagerCurrentPage++;
                this.dataGrid.view.seekToPage = this._customPagerCurrentPage;
                this.dataGrid.refresh();
                this._originalPager.find(".custompager-curpage").text(this._customPagerCurrentPage);
                return;
            });

            dg.view.onDataChanged.subscribe(() => {
                this.updatePageControls(!$(this._btnSwitch).is(":checked"));
            });

            // save setting
            var oldCurrentSettings = (dg as any).getCurrentSettings;

            (dg as any).getCurrentSettings = function (flag) {
                var settings = oldCurrentSettings.apply(dg, [flag]);
                settings['customPagerMode'] = $(btnSwitch).is(":checked") ? 'full' : 'next-previous-only';

                return settings;
            };

            var oldRestoreSettings = (dg as any).restoreSettings;

            (dg as any).restoreSettings = function (settings, flags) {
                oldRestoreSettings.apply(dg, [settings, flags]);
                if (settings == null) {
                    var storage = this.getPersistanceStorage();
                    if (storage == null) {
                        self.switchView(self._pagingMode);
                        return;
                    }
                    var json = Q.trimToNull(storage.getItem(this.getPersistanceKey()));
                    if (!json) {
                        self.switchView(self._pagingMode);
                        return;
                    }
                    settings = JSON.parse(json);
                }


                var viewPagerMode = settings.customPagerMode || self._pagingMode;
                var currentViewPagerMode = $(btnSwitch).is(":checked") ? 'full' : 'next-previous-only';

                if (viewPagerMode != currentViewPagerMode) {
                    $(btnSwitch).click();
                }
            };
        }

        public updateNextButton(nbrOfRecords: number, nbrOfRowsPerPage: number): void {
            if (this.options.pagingMode === 'full') {
                return;
            }

            if (nbrOfRecords == 0 || nbrOfRecords < nbrOfRowsPerPage) {
                this._originalPager.find(".custompager-next").prop("disabled", true);
                this._originalPager.find(".custompager-next").css("opacity", 0.5);
            }
            else {
                this._originalPager.find(".custompager-next").prop("disabled", false);
                this._originalPager.find(".custompager-next").css("opacity", 1);
            }
        }

        private switchView(pMode: ('full' | 'next-previous-only')): void {
            this.updatePageControls(pMode == "next-previous-only");
            this.dataGrid.refresh();
            (this.dataGrid as any).persistSettings();
        }

        private updatePageControls(isNextPreviousOnlyMode: boolean) {
            if (isNextPreviousOnlyMode) {
                this._originalPager.find(".next-previous-pager").show();
                this._originalPager.find(".slick-pg-grp").hide();
                this._originalPager.find(".slick-pg-sep").hide();
                this._originalPager.find(".slick-pg-grp:first").show();
            }
            else {
                this._originalPager.find(".next-previous-pager").hide();
                this._originalPager.find(".slick-pg-grp").show();
                this._originalPager.find(".slick-pg-sep").show();
            }

            $(".slick-pg-in").show();
        }

        public getCurrentPagerMode(): ('full' | 'next-previous-only') {
            return $(this._btnSwitch).is(":checked") ? 'full' : 'next-previous-only';
        }
    }

    export class CustomPagerWithOnlyNextPreviousMixinOptions<TItem> {
        grid: Serenity.DataGrid<TItem, any>;
        rowPerPage: number;
        pagingMode?: ('full' | 'next-previous-only');
    }
}

Endpoint.cs

public ListResponse<MyRow> List(IDbConnection connection, MyBaseListRequest request)
{
    return new MyRepository().List(connection, request);
}

Repository.cs

public ListResponse<MyRow> List(IDbConnection connection, MyBaseListRequest request)
{
    return new MyListHandler().Process(connection, request);
}

public class CustomListRequestHandle<TRow> : ListRequestHandler<TRow> where TRow : Row, new()
{
    protected override void ApplyFilters(SqlQuery query)
    {
        base.ApplyFilters(query);

        if (Request is MyBaseListRequest customRequest)
        {
            if (customRequest.EnableOnlyNextPreviousMode)
            {
                query.ApplySkipTakeAndCount(this.Request.Skip, this.Request.Take, this.Request.ExcludeTotalCount || DistinctFields != null);

                // Setting CountRecords to false stops the count(*) query from running
                query.CountRecords = false;
            }
        }                
    }
}

Grid.ts

/// <reference path="../../common/mixin/custompagerwithonlynextpreviousmixin.ts" />

private _pagerMixin: Common.CustomPagerWithOnlyNextPreviousMixin<Your_Row>;

protected onViewProcessData(response: Serenity.ListResponse<Your_Row>): Serenity.ListResponse<Your_Row> {
    var lr = super.onViewProcessData(response);

    this._pagerMixin.updateNextButton(lr.Entities.length, response.Take);

    return lr;
}

protected getViewOptions() {
    var opt = super.getViewOptions();
    opt.rowsPerPage = 20;
    return opt;
}

protected createToolbarExtensions(): void {
    super.createToolbarExtensions();
    var self = this;

    this._pagerMixin = new Rydell.Web.Common.CustomPagerWithOnlyNextPreviousMixin({
        grid: this,
        rowPerPage: this.getPagerOptions().rowsPerPage
    });
}

protected onViewSubmit() {
    if (!super.onViewSubmit()) {
        return false;
    }
    var request = this.view.params as MyBaseListRequest;
    request.EnableOnlyNextPreviousMode = this._pagerMixin.getCurrentPagerMode() == 'next-previous-only';
    return true;
}

protected getPersistanceStorage(): Serenity.SettingStorage {
    return new Common.UserPreferenceStorage();
}

Feel free to ask any question 馃挅

@minhhungit ,

I have allowed myself to change the title of your wiki entry to: Grid: .... (categorized it under Grid for being better findable)

Maybe you want to update your link in this post to: https://github.com/volkanceylan/Serenity/wiki/Grid:-Custom-grid-&-grid-pager-without-counting-total-records

And if you don't mind: Put also your mixin code into your wiki - as you say there that it is beter to do a mixin. :-)

With kind regards,

John

@JohnRanger I will update wiki soon, thank you !

@minhhungit - thanks very much :-)

John

Just noticed one issue.
image

When i click on Edit button (form mode) then switch back to grid, then an additional set of previous/next buttons are displayed. On this screenshot, i switched several times and got this screen (all buttons act the same but are duplicated).
When clicking on button i am getting a :
image

Did i miss something ?

@kilroyFR I can not reproduce it, I tried some times on some my projects with I don't your problem.
Can you provide your code, in private mode if you want, I might can help

Issue occurs when you have others GRIDS in tabs (looks like the grids in the tabs inherit from the main grid)

@kilroyFR I can reproduce the issue now, will try to fix

sorry I was quite busy so I couldn't reply you sooner

Wow Much Awesome, looks like it works like a charm. Many thanks !

Was this page helpful?
0 / 5 - 0 ratings

Related issues

moostafaa picture moostafaa  路  3Comments

gfo2007 picture gfo2007  路  3Comments

AmuthaKondusamy picture AmuthaKondusamy  路  3Comments

chintankukadiya18 picture chintankukadiya18  路  3Comments

john20xdoe picture john20xdoe  路  3Comments