Building Custom Paging with LINQ, ListView, DataPager and ObjectDataSource

by mosessaur| 22 June 2008| 23 Comments

Last week I posted about building custom paging with LINQ to SQL. And I wrapped the functionality with Extension Method to IQueryable<T> Interface.

Today I'm going to put the custom paging in a practical sample using ASP.NET ListView, DataPager and ObjectDataSource Control. Click here to download the sample. View demo here.

The sample is using Northwind Database. So first I created a Northwind LINQ to SQL class ".dbml"

NorthwindDataContext

Also I created the Extension Method that will handle paging. The following code shows the Extension Method:

/// <summary>
/// Perform custom paging using LINQ to SQL
/// </summary>
/// <typeparam name="T">Type of the Datasource to be paged</typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="obj">Object to be paged through</param>
/// <param name="page">Page Number to fetch</param>
/// <param name="pageSize">Number of rows per page</param>
/// <param name="keySelector">Sorting Expression</param>
/// <param name="asc">Sort ascending if true. Otherwise descending</param>
/// <param name="rowsCount">Output parameter hold total number of rows</param>
/// <returns>Page of result from the paged object</returns>
public static IQueryable<T> Page<T, TResult>(this IQueryable<T> obj, 
								int page, int pageSize, 
								Expression<Func<T, TResult>> keySelector, 
								bool asc, out int rowsCount)
{
	rowsCount = obj.Count();
	int innerRows = rowsCount - (page * pageSize);
	if(asc)
		return obj.OrderByDescending(keySelector)
							.Take(innerRows)
							.OrderBy(keySelector)
							.Take(pageSize)
							.AsQueryable();
	else
		return obj.OrderBy(keySelector)
							.Take(innerRows)
							.OrderByDescending(keySelector)
							.Take(pageSize)
							.AsQueryable();
}

Note that this method is Generic Method, so it can be used with any collection implements IQueryable<T>. The method also support sorting in both directions ASC or DESC. So using this method, you don't have to write specific stored procedure or tabular function for each Table or View in your Database to implement custom database paging that support sorting too.

To be able to use ObjectDataSource in proper way, I made a simple Business Class that wrap the NorthwindDataContext -Northwind LINQ to SQL Class- and called it NorthwindProducts. This class basically contain 2 static methods one that returns total rows in Products table "GetProductsCount", and the other return paged results "GetProductsPage".

The 2 methods are shown below:

public partial class NorthwindProducts
{
	public static int GetProductsCount()
	{
		using (var dc = new NorthwindDataContext())
		{
			int i = dc.Products.Count();return i;
		}
	}
	public static IEnumerable<Product> GetProductsPage(int rowIndex, int pageSize, string sortExpression)
	{
		using (var dc = new NorthwindDataContext())
		{
			int totalRows;
			int pageIndex = rowIndex / pageSize;   
			var products = new List<Product>();
			IEnumerable<Product> productsPage;
			bool asc = !sortExpression.Contains("DESC");
			switch (sortExpression.Split(' ')[0])
			{
				case "ProductName":
					productsPage = dc.Products
						.Page(pageIndex, pageSize, p => p.ProductName, asc, out totalRows);
					break;
				case "QuantityPerUnit":
					productsPage = dc.Products
						.Page(pageIndex, pageSize, p => p.QuantityPerUnit, asc, out totalRows);
					break;
				case "UnitPrice":
					productsPage = dc.Products
						.Page(pageIndex, pageSize, p => p.UnitPrice, asc, out totalRows);
					break;
				case "UnitsInStock":
					productsPage = dc.Products
						.Page(pageIndex, pageSize, p => p.UnitsInStock, asc, out totalRows);
					break;
				case "UnitsOnOrder":
					productsPage = dc.Products
						.Page(pageIndex, pageSize, p => p.UnitsOnOrder, asc, out totalRows);
					break;
				default:
					productsPage = dc.Products
						.Page(pageIndex, pageSize, p => p.ProductID, asc, out totalRows);
					break;
			}
			foreach (var product in productsPage)
			{
				products.Add(product);
			}
			return products;
		}
	}
}

Let's explore GetProductsPage method. This method will return IEnumrable<Product>. It accepts 3 parameters, rowIndex, pageSize and sortExpression:

  • rowIndex: Index of the the next first record (row) in the result set.
  • pageSize: Total number of records (rows) to be displayed (retrieved) in the result set.
  • sortExpression: String that describe sort expression. Here I pass the sort expression constructed by ListView. For example if the sort is ascending the sort expression will be "ColumnName" while if the sort is descending the expression will be "ColumnName DESC".

That was all about core code that will perform paging. Its time to go through the presentation and how to use ListView and DataPager along with ObjectDataSource that will use the above mentioned methods.

For the UI design I used Matt's VS2008 Styled Grid styles to form the UI, so you can return to that post if you have any enquiry regarding UI styles.

I'll start with the ObjectDataSource the code is show below:

   1: <asp:ObjectDataSource ID="odsProducts" runat="server" EnablePaging="True" 
   2:     MaximumRowsParameterName="pageSize" SelectCountMethod="GetProductsCount" 
   3:     SelectMethod="GetProductsPage" StartRowIndexParameterName="rowIndex" 
   4:     TypeName="NorthwindProducts" SortParameterName="sortExpression">
   5: </asp:ObjectDataSource>

As show on the code snippet:

  • TypeName is "NorthwindProducts" the name of my business class.
  • SelectCountMethod is "GetProductsCount".
  • SelectMethod is "GetProductsPage".
  • MaximumRowsParameterName is "pageSize", this is the parameter passed to the "GetProductsPage".
  • StartRowIndexParameterName is "rowIndex", this is also parameter passed to the "GetProductsPage".
  • SortParameterName is "sortExpression", again another parameter to be passed to the "GetProductsPage".

Nothing more on ObjectDataSource. Note that mapping between GetProductsPage parameters and MaximumRowsParameterName, StartRowIndexParameterName and SortParameterName properties of ObjectDataSource.

So Time to explore ListView with DataPager and link between ListView and ObjectDataSource:

   1: <asp:ListView ID="lvProducts" runat="server" DataSourceID="odsProducts" >
   2:     <LayoutTemplate>
   3:         <table class="datatable" cellpadding="0" cellspacing="0" border="0">
   4:             <tr>
   5:                 <th class="center">
   6:                     <asp:LinkButton Text="ID" CommandName="Sort" CommandArgument="ProductID" runat="server" />
   7:                 </th>
   8:                 <th class="first">
   9:                     <asp:LinkButton Text="Name" CommandName="Sort" CommandArgument="ProductName" runat="server" />
  10:                 </td>
  11:                 <th class="first">
  12:                     <asp:LinkButton Text="Quantity" CommandName="Sort" CommandArgument="QuantityPerUnit" runat="server" />
  13:                 </th>
  14:                 <th class="right">
  15:                     <asp:LinkButton Text="Unit Price" CommandName="Sort" CommandArgument="UnitPrice" runat="server" />
  16:                 </th>
  17:                 <th class="center">
  18:                     <asp:LinkButton Text="In Stock" CommandName="Sort" CommandArgument="UnitsInStock" runat="server" />
  19:                 </th>
  20:                 <th class="center">
  21:                     <asp:LinkButton Text="On Order" CommandName="Sort" CommandArgument="UnitsOnOrder" runat="server" />
  22:                 </th>
  23:             </tr>
  24:             <asp:PlaceHolder ID="itemPlaceholder" runat="server"></asp:PlaceHolder>
  25:             <tr>
  26:                 <td colspan="6">
  27:                     <asp:DataPager ID="lvProductsPager" PagedControlID="lvProducts" PageSize="10" runat="server">
  28:                         <Fields>
  29:                             <asp:NextPreviousPagerField ShowPreviousPageButton="true" ShowFirstPageButton="true"  ShowNextPageButton="false" ShowLastPageButton="false"/>
  30:                             <asp:NumericPagerField />
  31:                             <asp:NextPreviousPagerField ShowPreviousPageButton="false" ShowFirstPageButton="false" ShowNextPageButton="true" ShowLastPageButton="true"/>
  32:                         </Fields>
  33:                     </asp:DataPager>
  34:                 </td>
  35:             </tr>
  36:         </table>
  37:     </LayoutTemplate>
  38:     <ItemTemplate>
  39:         <tr class='row'>
  40:             <td class="first">
  41:                 <%# Eval("ProductID")%>
  42:             </td>
  43:             <td>
  44:                 <%# Eval("ProductName")%>
  45:             </td>
  46:             <td>
  47:                 <%#Eval("QuantityPerUnit")%>
  48:             </td>
  49:             <td>
  50:                 <div class="money"><%#Eval("UnitPrice", "{0:C2}")%></div>
  51:             </td>
  52:             <td class="center">
  53:                 <%#Eval("UnitsInStock")%>
  54:             </td>
  55:             <td class="center">
  56:                 <%#Eval("UnitsOnOrder")%>
  57:             </td>
  58:         </tr>
  59:     </ItemTemplate>
  60: </asp:ListView>

In the LayoutTemplate I declared TH tags that contains LinkButton with CommanName set to Sort and CommandArgument set to sort expression for each column. Also I declared a DataPager inside the ListView LayoutTemplate.

Now the sample is ready to run.

Conclusion:
Custom paging is required by many application, and is implemented with different approaches. The approach represented here is using LINQ to SQL to construct database paging query. It is not the best, but it is effective because using this method you can custom paging and sorting with generic method. So you don't have to create stored procedure of each table you wish to page through.

kick it on DotNetKicks.com

Comments (23) -

Josh Stodola
Josh Stodola United States on 6/13/2008 3:39 AM This is some pretty cool and useful stuff you've been posting about lately.  Thanks for sharing your knowledge!  I just wanted to let you know that for some reason, the code snippets in your RSS feed are double-spaced.
mosessaur
mosessaur Egypt on 6/14/2008 12:32 AM Thank you Josh! And thank you for telling me about the RSS issue. I'm going to check it out.
Marius
Marius United States on 6/22/2008 3:08 AM Thanks a lot! I was looking for this!
Martillo
Martillo United States on 7/1/2008 9:43 AM Nice! Thanks for the example. Your blog rocks! Great combination of hard-core developer know-how and a sense of style.

Check out this Listview I found on CodeProject that adds sort indicators to column headers: www.codeproject.com/KB/webforms/ListViewSort.aspx

Hey, here's an idea for the CSS in this example: instead of applying the "first" class to the first TD in each row and then using style rules like this:

.grid .datatable .row TD.first

or this:

.grid .datatable .row:hover .first

you could eliminate the "first" class altogether and do this:

.grid .datatable tr.row td:first-child

or this:

.grid .datatable tr.row:hover td:first-child

Those rules should work in IE 7 and FF 2+.



mosessaur
mosessaur Egypt on 7/1/2008 5:25 PM Thank you so much Martillo, and thank for exploring the code and update me with these information.
Actually I just cloned the styles from http://mattberseth.com, but I'm gonna try your suggestions.
Thank you once more
Emir Trevi&#241;o
Emir Treviño Mexico on 7/6/2008 6:48 PM Good post, the datapager control is more "public web site"-friendly, is more easy share the urls and work with cache...

I can announce my post? This more focused on Entity Framework and the GridView.
emir.com.mx/.../...tity-Framework-in-N-Layers.aspx

Saludos!
mosessaur
mosessaur Egypt on 7/7/2008 2:46 PM @Emir: I read it, and really liked it Emir. Good one, already kicked! ;)
jordan
jordan Canada on 7/23/2008 2:23 AM Excellent stuff here Muhammad.

I was wondering where i would add the where clause? I assume the business layer. Lets say we want all the products $25 or more.

productsPage = dc.Products.Page(pageIndex, pageSize, p => p.ProductName, asc, out totalRows).Where( p => p.UnitPrice >=25);

Is that correct? i thought it would affect the .Page() innerRows Count.


mosessaur
mosessaur Egypt on 7/23/2008 5:18 AM Well, someone might of it as filtering the data first then perform paging on the filtered results. I need to check that out. and by the way there is more efficient way than the one discussed here. check out at this post mosesofegypt.net/.../...fferent-Paging-Method.aspx
Shawn
Shawn United States on 9/5/2008 8:58 PM Hi Muhammad. Excellent article! I've been fooling around with your code sample all day and have a question.

If we wanted to pass in a LINQ expression that contains a join and some more advanced logic how would we do that using the Page() method? Say I wanted to use this expression:

var q =
     from v in dc.Table_Videos
     join u in dc.Table_Users on v.userId equals u.userId
     select u;

Would something simple like this work:

productsPage = dc.Products.Page(pageIndex, pageSize, q, asc, out totalRows)

That looks too good to be true!

Thanks again for the excellent articles. You rock.
mosessaur
mosessaur Egypt on 9/6/2008 12:10 AM @Shawn Hey Shawn, Actually am not sure of this would work, but there is a better way I think. you are calling dc.Products.Page and then passing your query as Parameter. Why don't you try it this way
q.Page(...) directly.
Of course you are expecting a specific type that return from Page method as a results. And your query should already return that type of results.
Try it I am sure is should work this way.
Shawn
Shawn United States on 9/6/2008 10:47 PM Hey Muhammad,

Thank you for your reply. I see now how your extension method will allow me to append the Page() function to my object directly. Rather brilliant, really.

Would you mind explaining how I can convert the query to return a specific type? When I call q.Page() I am getting this error:

Cannot implicitly convert type 'System.Linq.IQueryable<AnonymousType#1>' to 'System.Collections.Generic.IEnumerable<Table_Video>'. An explicit conversion exists (are you missing a cast?)

Thank you!
mosessaur
mosessaur Egypt on 9/7/2008 10:34 PM You query should return IQueryable<Something>. I guess it should be IQuerable<User> according to your code.
So it should work without modifying any thing. The Method is attached to IQueryable<T> so any thing that is IQueryable<T> should be able to use it.
Could you please provide me with more code, maybe by using contact for and attache your code.
kclmnop
kclmnop Canada on 9/20/2008 1:45 AM Great for small databases, where the need for data paging is minimal, but completely unusable for large data sets.

Think about a scenario where you have 1 billion rows... Your use of predicates will fetch, when asked for the middle of the data set, a maximum of 500 million records.

The purpose of paging is to store locally a minimal amount of data and to preserve as much as possible bandwidth.  Your sample will fetch from the SQL server around half of the data when seeking for the middle records, which really isn't what data paging is about.

mosessaur
mosessaur Egypt on 9/20/2008 1:51 AM @kclmnop There is another approach using again LINQ posted here as well
mosesofegypt.net/.../...fferent-Paging-Method.aspx
However if you think that this method is not also efficient, I wish to know how do you perform paging using SQL with Stored Procedures on SQL Server or any other Database.
Shawn
Shawn United States on 9/20/2008 5:20 AM I kind of agree with kclmop. I've tried almost every tricky LINQ to SQL paging method as well as the inherent paging controls (DataPager for example) and I always find my self reverting to the tried and true SQL ROW_NUMBER() OVER method, which seems to work best. What LINQ needs is an implementation of that command.
mosessaur
mosessaur Egypt on 9/20/2008 5:26 AM @Shawn Refer to this post mosesofegypt.net/.../...fferent-Paging-Method.aspx . It is generating that kind of query you are talking about that is using ROW_NUMBER() OVER
Ryan
Ryan United States on 10/3/2008 4:52 PM Thanks for the help
Cord
Cord United States on 10/24/2008 8:42 AM Your code really helped me fix the paging problem....

Thanks buddy.
Hasan Raza
Hasan Raza India on 12/24/2008 10:42 AM Thanks Mosa,
I think this should work in an MVC app

thanks again

Eric
Eric United Kingdom on 6/7/2009 5:47 PM Thanks Mosa,

Nice stuff. Your qualifications are impressive. Keep up the good work!

Eric
mosessaur
mosessaur Egypt on 6/7/2009 10:39 PM Thank you Eric for the complement.
JATIN
JATIN India on 7/29/2009 5:15 AM i downloaded ur sample but i am not able to use it
.it keeps on giving errors
like
Error  68  Class, struct, or interface method must have a return type  
Error  69  Identifier expected  
Error  64  Invalid token ')' in class, struct, or interface member declaration  

pls help me
i am using vs2005 nad .net framework 2.0



Pingbacks and trackbacks (2)+

Comments are closed