Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Dynamic Sorting in LINQ and VB, Part 3: Pagination

4.89/5 (3 votes)
28 Oct 2013CPOL3 min read 12.2K  
Dynamically sort query results using LINQ expressions and reflection with sorting and paging using VB.NET.

Introduction

Pagination is a typical process in data display whereby large sets of data are broken into discrete sections for viewing, more often than not used in conjunction with some type of grid or list component.  In Part 1 and Part 2 of this series, we walked through the process of dynamic sorting using LINQ to return a sorted collection of objects.  In this tip, we'll walk through the basics of how to add pagination into the mix.

Using the Code

In this tip, we're going to use the basic data model that was defined as a structure in the previous tip:

VB.NET
Public Structure Person
    Public Property FirstName() As String
    Public Property MiddleInitial() As String
    Public Property LastName() As String

    Public Overrides Function ToString() As String
        Return String.Format("{0}, {1} {2}", LastName, FirstName, MiddleInitial)
    End Function
End Structure

Next, we'll set up the data store containing a list of these objects:

VB.NET
Public Class PeopleDataStore
	Public Const SortAscending As Boolean = True
	Public Const SortDescending As Boolean = False

	Private m_people As List(Of Person) = Nothing

	Public Sub New()
		m_people = New List(Of Person) From {
			New Person() With { .FirstName = "John", .MiddleInitial = "Q", .LastName = "Doe" },
			New Person() With { .FirstName = "Jane", .MiddleInitial = Nothing, .LastName = "Appleton" },
			New Person() With { .FirstName = "Jim", .MiddleInitial = "H", .LastName = "Smith" },
			New Person() With { .FirstName = "Joe", .MiddleInitial = Nothing, .LastName = "Plumber" }
		}
	End Sub

	Public ReadOnly Property People() As List(Of Person)
		Get
			Return m_people
		End Get
	End Property

	Public Function GetPeople(Optional ByVal sortPropertyName As String = Nothing, Optional ByVal sortAscending As Boolean = True, Optional ByVal pageIndex As Integer = 0, Optional ByVal pageSize As Integer = 25) As List(Of Person)
		Dim actualIndex As Integer = (pageIndex * pageSize)
		Dim list As List(Of Person) = m_people

		If (String.IsNullOrEmpty(sortPropertyName) = False) Then
			Dim personType As Type = GetType(Person)

			If (personType.GetProperties().Any(Function(prop) prop.Name = sortPropertyName AndAlso prop.CanRead)) Then
				Dim pinfo As PropertyInfo = personType.GetProperty(sortPropertyName)
				Dim paramExpr As ParameterExpression = Expression.Parameter(GetType(Person), "instance")
				Dim memberExpr As MemberExpression = Expression.Property(paramExpr, pinfo)

				Dim orderByFunc As Func(Of Person, Object) = Expression.Lambda(Of Func(Of Person, Object))(memberExpr, paramExpr).Compile()
				Dim sortFunc As Func(Of IEnumerable(Of Person), IOrderedEnumerable(Of Person)) = Nothing

				If (sortAscending) Then
					sortFunc = (Function(source) source.OrderBy(orderByFunc))
				Else
					sortFunc = (Function(source) source.OrderByDescending(orderByFunc))
				End If

				list = sortFunc(list).ToList()
			End If
		End If

		If (actualIndex <= (list.Count + pageSize)) Then
			Return list.Skip(actualIndex).Take(pageSize).ToList()
		Else
			Return list
		End If
	End Function
End Class

Taking a walk through the code, we start out by defining a couple of Boolean constants for better legibility when calling the GetPeople method; either these constants or literal Boolean values can be used for the sortAscending parameter on the method.  Next, we create a strongly-typed list of the Person type to hold our data values, then populate the list in the constructor; providing a corollary read-only property of the same type for access to the list.

The GetPeople method is defined with four optional parameters: the name of the public property from the Person structure used for sorting, a Boolean flag indicating whether the list should be sorted in ascending or descending order, an integer value for the starting index of the paging operation, and an integer value for the size of the page collected from the paging operation.  The paging values are determined by the pageIndex and pageSize parameters: the pageIndex parameter defines where in the collection the paging operation should start while the pageSize parameter defines the number of elements to be retrieved from the collection.  Since the code will not have any notion of where to begin taking elements from the collection, the actualIndex variable is defined based on creating an index which is the product of the number of elements multiplied by the requested page index.

If no values have been supplied for any of the parameters, the method simply returns the list values in their default order.  If the sortPropertyName parameter is supplied, we then check to see if the Person type defines a readable public property of the same name and continue processing should this prove true.  From there, we obtain a PropertyInfo instance from the Person type to be used in the sorting operation.  At this point, the code becomes divergent from the prior tips in that we begin constructing a dynamic lambda expression as the delegate parameter for the OrderBy or OrderByDescending extension methods; the first portion of the expression is a ParameterExpression instance which creates the main parameter for the lambda expression.  Next, we create a MemberExpression instance which identifies the desired property from the parameter value.  The orderByFunc variable is populated by the compiled result of the lambda expression assembly call.

Next, we create another Func delegate to define which sorting method to call based on the sortAscending parameter supplied to the method call.  If no value is supplied for the parameter, collection sorting defaults to sort ascending.  Since we know that the OrderBy and OrderByDescending methods are extensions supplied to the IEnumerable(T) interface and our list implements this interface, we define the Func delegate to access an input parameter of List(Of Person).  The output parameter is then defined as the same return type as defined by the OrderBy and OrderByDescending methods, this being an instance of IOrderedEnumerable(T).  With the delegate defined, we then apply sorting to the list.

Finally, we perform a sanity check on the paging values supplied against the number of elements in the source list.  If the list contains fewer elements than is requested by the paging values, the entire list is returned.  If the list contains more element than the paging values, the result of applying the Skip and Take extension methods is returned.

VB.NET
Module Program
	Public Shared Sub Main()
		Dim consoleAction As Action(Of Person) = (Function(person) Console.WriteLine(Person))
		Dim dataStore As PeopleDataStore = New PeopleDataStore()

		Console.WriteLine("People listed in default order:")
		dataStore.People.ForEach(AddressOf consoleAction)

		Console.WriteLine("{0}People listed in reverse alphabetical order by last name:", Environment.NewLine)
		dataStore.GetPeople("LastName", PeopleDataStore.SortDescending, 0, 2).ForEach(AddressOf consoleAction)
	End Sub
End Module

Further Reading

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)