‫Custom Paging بهینه و غیر Declarative در DataGrid

اگر بخواهید از الگوی MVP در برنامه‌های ASP.NET استفاده کنید بایستی بتوانید همه چیز را به CodeBehind انتقال دهید. این یعنی declarative و binding تقریباً تعطیل. یکی از امکانات مهمی که این وسط از دست می‌رود، امکان Paging بهینه کنترل GridView از طریق CodeBeind است. منظور از Paging بهینه فقط نمایش pager در قسمت پایین کنترل نیست، بلکه منظور این است که فقط رکوردهای مورد نیاز برای صفحه جاری (معمولاً ۱۰ تا ۲۰ تا) از دیتابیس فراخوانی شود. کنترل GridView از طریق DataSourceهای موجود در markup مثل ObjectDataSource یا SqlDataSource می‌تواند Paging بهینه را ارائه دهد. اما از طریق CodeBehind، یعنی handle کردن دستی event مربوط به Paging فقط یک Paging غیر بهینه ارائه می‌دهد. چون در CodeBehind راهی برای اعلام تعداد رکوردها به GridView وجود ندارد (+).

می‌شود به جای GridView از کنترل DataGrid استفاده کرد. DataGrid امکان Paging بهینه از داخل Code Behind را به خوبی پشتیبانی کرده و می‌توان آن را با استفاده از الگوی MVP به کار گرفت. برای این کار کافیست از eventهای SortCommand و PageIndexChanged و پراپرتی‌های VirtualItemCount و CurrentPageIndex استفاده نمایید. لطفاً نمونه زیر را ببینید.

‫Paging کوئری‌های SQL در NHibernate

تصور کنید تعدادی کوئری SQL دارید که می‌خواهید Paging را با استفاده از NHibernate در آنها فعال کرده و بنا به دلایلی اجازه بازنویسی آنها با دیگر APIهای NHibernate نداشته و آنها را صرفاً باید از طریق ObjectDataSaource به کنترل‌های ASP.NET بخورانید. سه راه برای انجام این کار وجود دارد.

راه اول:
استفاده از stored procedureهای کمکی که خاص این قضیه ایجاد شده‌اند. یکی از آنها Paging_RowCount است که توسط دوستم مسعود رمضانی معرفی شده. این ابزارها queryی (و احتمالاً stored procedure) شما دریافت کرده و Paging را روی آنها اعمال می‌کنند. بدی این ابزارها این است که محدودیت‌های زیادی در query دریافتی اعمال و کرده و در خیلی از حالات کار نمی‌کنند. بدی دیگر آنها عدم خوانایی و قابلیت نگهداری پایین آنهاست چون Query ورودی این ابزارها معمولاً به صورت تیکه تیکه در داخل کد C#‎ نگهداری می‌شود. البته من چندان تخصصی روی SQL ندارم و با کل این ابزار به طور کامل کار نکرده‌ام اما همین دلیل دوم برای من کافی است تا از آن دوری کنم.

راه دوم:
استفاده از Named SQL Queries در NHibernate است. در این روش شما می‌توانید با استفاده از SetFirstResult و SetMaxResults وظیفه Paging را به عهده NHibernate بگذارید. در این حالت NHibernate خودش به طور خودکار و پشت پرده مشابه روش اول عمل کرده و Paging را برای شما مهیا می‌نماید. این روش بسیار زیبا، راحت، خوانا و قابل نگهداری است اما بدی آن این است که در بعضی حالات که Query کمی پیچیده می‌شود عمل نمی‌کند. یعنی این که بیشتر برای Queryهای ساده مناسب است.

راه سوم:
وقتی که نمی‌خواهید از روش اول استفاده کنید و روش دوم هم درست کار نمی‌کند مجبورید خودتان وظیفه Paging را به عهده بگیرید. یعنی کاری را که روش‌های اول و دوم به صورت خودکار انجام می‌دادند شما به صورت دستی انجام دهید. روش معمول برای Paging در MS SQL Server که راه‌های اول و دوم هم از آن استفاده می‌کنند روش ROW_NUMBER()‎ است. من خودم برای انجام این کار از یک Query حاضر و آماده که توسط NHibernate تولید می‌شد استفاده می‌کنم. به نمونه اسکریپت زیر توجه فرمایید:


SELECT TOP (@maxResults) * FROM (

select

*

,ROW_NUMBER() OVER(ORDER BY (@orderBy)) as _sort_row from

Toy

)as query WHERE query._sort_row > @firstResult
ORDER BY query._sort_row
البته من همین query را هم در داخل یک Named SQL Query قرار داده‌ام ولی به جای فراخوانی توابع SetFirstResult و SetMaxResults از پارامترهایی که خودم به همین اسامی در query تعبیه کرده‌ام استفاده می‌کنم.

Paging problem with Named SQL queries

Named SQL query in NHibernate is an easy way to apply paging through native SQL. SetFirstResult and SetMaxResults helps a lot here. But the problem is when query is get a bit complicated or for some other reasons that I don’t know, paging does not works on pages after first page. In this case a wrong sql query is generated.

From given answers I didn’t realized what’s the root cause and if there is any solutions or not. But because I was in emergency I used my work-around for it.

My work-around is adding paging elements directly in the query and pass “first result” and “max results” to the query as parameters instead of using SetFirstResult and SetMaxResults. Consider following example:





default-access="field.camelcase"
namespace="nhtest"
assembly="nhtest">



SELECT TOP (:maxResults) * FROM (

select

*

,ROW_NUMBER() OVER(ORDER BY (:orderBy)) as _sort_row from

Toy

)as query WHERE query._sort_row > :firstResult
ORDER BY query._sort_row

]]>





This code is inspired from script that NHibernate itself produces while correct paging. Parametrization “top” and “order” sections was not very easy. I got helped from here and here. Notice parametrized “order” is not necessary for paging. It was my own requirement.

Paging methods

ASP.NET GridView and other similar controls that support paging needs special methods that return only the requested slice of data and  the count of total data. As I’m a lover of NHibernate/Castle ActiveRecord I have gathered paging techniques in NHibernate HQL, LINQ-to-NHibernate and Castle ActiveRecord:

NHIbernate HQL:

public IList GetData(int page, int pageSize, ref long count)
{
    IList result = null;
    string query_string = “from my_class”;
    IMultiQuery query = session.CreateMultiQuery()
        .Add(session.CreateQuery(query_string)
        .SetFirstResult((page – 1) * pageSize)
        .SetMaxResults(pageSize))
        .Add(“select count(*) ” + query_string);
    IList list = query.List();
    result = (IList)list[0];
    count = (long)((IList)list[1])[0];
    return result;
}

LINQ-to-NHibernate:

internal List GetData(int page, int pageSize)
{

    var q = from t in session.Linq()
            select t;

    return q
        .Skip((page – 1) * pageSize)
        .Take(pageSize)
        .ToList();
}

Castle ActiveRecord:

public static Company[] FindAll(int maximumRows, int startRowIndex, string sortExpression)
{
    Order[] orders;

    if (string.IsNullOrEmpty(sortExpression))
        orders = new Order[0];
    else
    {
        orders = new Order[1];
        const string DESC = ” DESC”;
        if (sortExpression.EndsWith(DESC))
            orders[0] = Order.Desc(sortExpression.Replace(DESC, string.Empty));
        else
            orders[0] = Order.Asc(sortExpression);
    }

    return SlicedFindAll(startRowIndex, maximumRows, orders);
}

public static int TotalCount()
{
    return Count();
}

For more info refer to this link, link, link and this link.

‫Paging بدون DataSource

بیشتر کارهای کنترل‌های ASP.NET را هم می‌توان به صورت Declartive و هم در Codebehind انجام داد. یکی از این کارها عملیات Paging در GridView است. متاسفانه یک محدودیت ظریف در حالت غیر Declarative وجود دارد که کار را برای کسانی که همه کارهای کنترل‌هایشان را در Codebehind انجام می‌دهند سخت کرده است.

اگر GridView را به یک DataSource مثل ObjectDataSource وصل کنید، آن DataSource از دو متود خاص برای بازیافت بهینه اطلاعات استفاده می‌کند. یکی متودی که برای دریافت فقط یک صفحه از اطلاعات و نه همه آنها به کار می‌رود و دیگری متودی برای دریافت تعداد کل رکوردها. اگر از DataSource استفاده نکنید باید event مربوط به PageIndexChanging را در codebehind مورد استفاده قرار دهید. کد این قسمت مشابه کد زیر خواهد بود:

protected void grdList_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
grdList.PageIndex = e.NewPageIndex;
grdList.DataSource = GetData(grdList.PageSize, e.NewPageIndex);
grdList.DataBind();
}

اما چون بدون استفاده از DataSource راهی وجود ندارد که به GridView بگوییم تعداد کل رکوردها چندتاست، پس مکانیزم Pager کار نخواهد کرد. این همان محدودیت ظریف است که در مکانیزم Paging مربوط به GridView وجود دارد. این محدودیت در این لینک MSDN هم توضیح داده شده است. برای کسب اطلاع بیشتر می‌توانید به این لینک و این لینک هم مراجعه کنید.

Castle ActiveRecord and GridView’s Paging and Sorting

Paging and sorting is a common need in ASP.NET applications. GridView itself have a default paging and sorting mechanism. Default paging has performance issues while manipulating large amounts of data. So people use a custom paging mechanism. This way they must note that only needed data must be extracted from database. For example when page 3 just shows 10 records from 21 to 30, there is no reason to load all data from database. With sorting there is two problem. First: default sorting mechanism only work with few specific data sources like Typed DataSet and not every other data sources like those come from NHibernate or Castle ActiveRecord. Second: default sorting sorts only current page not all data.

In order to have an efficient paging and proper sorting mechanism we should use custom paging and custom sorting. Scott Mitchell has a great tutorial series on paging and sorting with GridView. These series are based on an object data source of typed DataSets. So as I’m working with Castle ActiveRecord as my data access layer, I was unable to use Scott’s solution. So I decided to create my own solution using Castle Active Record based on Scott’s original solution.

Doing paging and sorting with Castle Active Record is very very easy. Because Castle ActiveRecord has an API dedicated to paging and sorting: SlicedFindAll. In my solution, firstly I have added 2 method to a typical domain class named Company, secondly notice GridView’s markup that has no codebehind at all. Notice that all my domain classes are inheriting from ActiveRecordBase:

public static Company[] FindAll(int maximumRows, int startRowIndex, string sortExpression)
{
    Order[] orders;

    if (string.IsNullOrEmpty(sortExpression))
        orders = new Order[0];
    else
    {
        orders = new Order[1];
        const string DESC = " DESC";
        if (sortExpression.EndsWith(DESC))
            orders[0] = Order.Desc(sortExpression.Replace(DESC, string.Empty));
        else
            orders[0] = Order.Asc(sortExpression);
    }

    return SlicedFindAll(startRowIndex, maximumRows, orders);
}

public static int TotalCount()
{
    return Count();
}
<asp:GridView runat="server" ID="gvCompany" AllowPaging="true" AllowSorting="true"
DataSourceID="odsCompany" DataKeyNames="Id">
<Columns>
<asp:BoundField DataField="Name" HeaderText="Name" SortExpression="Name" />
<asp:BoundField DataField="Address" HeaderText="Address" SortExpression="Address" />
<asp:BoundField DataField="Tel" HeaderText="Tel" SortExpression="Tel" />
<asp:BoundField DataField="Field" HeaderText="Field" SortExpression="Field" />
</Columns>
</asp:GridView>
<asp:ObjectDataSource runat="server" ID="odsCompany" TypeName="MyDomainNamespace.Company"
SelectMethod="FindAll" EnablePaging="True" SelectCountMethod="TotalCount" SortParameterName="sortExpression" />