Data Sorting and Paging

Although sorting and paging functionality is already available in ASP.NET, there is not a consistent approach for all controls. Taking paging as an example, the ListView has the DataPager control; the GridView internally renders its own pager; whilst the Repeater has no out-of-the-box paging mechanism.

The Navigation Framework provides two controls called Sorter and Pager (the Pager is not available in VS 2005). These both only use StateContext Data to get and set their information, e.g., sort expression and page number; and hence they are not dependent on any controls, unlike the ASP.NET DataPager which needs knowledge of the ListView Control it is paging through. Since sorting and paging information is the same as any other StateContext Data, the same DataBinding techniques can be used and the Crumb Trail will automatically retain the sort expression and page number.

ASP.NET is based around the PostBack model and so has trouble implementing sorting and paging using Hyperlinks (although the DataPager does make an attempt). Since sorting and paging in the Navigation Framework use StateContext Data only, it does not matter whether a PostBack or (Hyperlink) Navigation occurred. However, with forward and backward so far being the only Navigation available how is it possible for sorting and paging to work via Hyperlink Navigation?

Taking the State Information configured in State Information, if page P1.aspx contained a Sorter control for example then the image below shows a dashed arrow for the desired Navigation.
Desired Refresh Navigation
Desired Refresh Navigation

To be able to navigate along the dashed arrow would require a new Transition to be added to the configuration as shown below. Clearly having to add such a hard-coded configuration every time a sorter or pager is used is undesirable, so a better solution is required.

<state key="S1" page="~/P1.aspx">
	<transition key="T1" to="S2"/>
	<transition key="Refresh" to="S1"/>
</state>
The Framework always allows Navigation from a State to itself without any extra configuration. This is considered refresh Navigation and to perform this there are two methods on the StateController class, called Refresh and GetRefreshLink. Analogously to their forward and backward counterparts, for Redirecting and Transferring the method required is Refresh, an overload of which accepts a NavigationMode parameter which specifies whether to Redirect or Transfer (the default is Redirect); and for a Hyperlink the method is GetRefreshLink. After a refresh Navigation the Crumb Trail is unchanged whilst the PreviousState is set to the current State.

However, refresh navigation by itself is not sufficient for sorting and paging to work. The image above shows, as well as refresh Navigation, that the Navigation Data passed must be the same as the current StateContext Data except for the changed sort expression. The Navigation Framework provides an easy way to create a copy of the current StateContext Data, simply pass true to the NavigationData constructor.

The code below shows an example of how a button could be used to do custom sorting (instead of using the Sorter control). It performs a refresh Navigation passing all the current StateContext Data except for the changed sort expression.

protected void SortButton_Click(object sender, EventArgs e)
{
	NavigationData toData = new NavigationData(true);
	toData["sortExpression"] = "Name DESC";
	StateController.Refresh(toData);
}

Sample Web Site

Sorting

To the Search method add a sortExpression parameter adorned with a NavigationDataAttribute and amend the Linq to implement sorting based on this parameter as shown below.


public IEnumerable Search(
	[NavigationData] string name, 
	[NavigationData] string minDateOfBirth, 
	[NavigationData] string sortExpression)
{
	var q = from p in _People
			where (name == null || p.Name.ToUpperInvariant().Contains(name.ToUpperInvariant()))
			&& (minDateOfBirth == null || p.DateOfBirth >= DateTime.Parse(minDateOfBirth))
			select new
			{
				p.Name,
				p.DateOfBirth,
				Link = StateController.GetNavigationLink("Select", new NavigationData() { { "id", p.Id } })
			};
	if (sortExpression != null)
		q = !sortExpression.EndsWith("DESC") ? q.OrderBy(p => p.Name) : q.OrderByDescending(p => p.Name);
	return q;
}
Pressing F5 will display the Person List but no sorting is applied since the sortExpression is currently always empty. A default value for the sortExpression parameter could be specified here, but this default value must also be stored in StateContext Data otherwise the Sorter Control would not be aware that is has been applied. Section ASP.NET Routing will cover default StateContext data.

Add a Sorter control to the HeaderTemplate of the Name column as shown below. This control will change the sortExpression in StateContext Data each time it is clicked. DataBinding will detect when the sortExpression parameter has changed and will rebind the GridView accordingly.


<asp:TemplateField HeaderText="Name">
	<HeaderTemplate><cc1:Sorter ID="Sorter1" runat="server" Text="Name" SortBy="Name" /></HeaderTemplate>
	<ItemTemplate>
		<asp:HyperLink ID="HyperLink1" runat="server" NavigateUrl='<%# Eval("Link") %>' Text='<%#: Eval("Name") %>' />
	</ItemTemplate>
</asp:TemplateField>
This time pressing F5 will display the Person List with a fully functioning Sort button. Selecting a Person and then the Hyperlink on the Details.aspx will return to the Listing.aspx with the Sort order retained, i.e., the Crumb Trail has remembered the Sort order.

Pressing the Search button should remove the Sort order, although currently it does not. Add a NavigationDataParameter to the UpdateParameters of the FormView’s NavigationDataSource as shown below, and set its Reset property to true. This time pressing F5, when the Search button is pressed the sortExpression is cleared.

<cc1:NavigationDataSource ID="NavigationDataSource1" runat="server">
	<UpdateParameters>
		<cc1:NavigationDataParameter Name="sortExpression" Reset="true"/>
	</UpdateParameters>
</cc1:NavigationDataSource>
Currently, clicking the Sorter control will cause a PostBack. However to change it to use refresh Navigation (Hyperlink) set its Navigate property to true as shown below. Pressing F5 will show the Sorter rendered as a Hyperlink. Enter search criteria to filter the list and click the Sorter to see that the search criteria are retained.

<HeaderTemplate><cc1:Sorter ID="Sorter1" runat="server" Text="Name" SortBy="Name" Navigate="true" /></HeaderTemplate>

Paging

The implementation of paging is similar to that of sorting. So to the Search method of the PersonSearch class add the startRowIndex and maximumRows as parameters and amend the Linq to use Skip and Take to apply paging logic based on these parameters as shown below. The parameters are nullable because they do not have default values, although this will be changed in Section Navigation Data Markup and Defaults where default StateContext data is covered.

public IEnumerable Search(
	[NavigationData] string name, 
	[NavigationData] string minDateOfBirth, 
	[NavigationData] string sortExpression,
	[NavigationData] int? startRowIndex,
	[NavigationData] int? maximumRows)
{
	var q = from p in _People
			where (name == null || p.Name.ToUpperInvariant().Contains(name.ToUpperInvariant()))
			&& (minDateOfBirth == null || p.DateOfBirth >= DateTime.Parse(minDateOfBirth))
			select new
			{
				p.Name,
				p.DateOfBirth,
				Link = StateController.GetNavigationLink("Select", new NavigationData() { { "id", p.Id } })
			};
	if (sortExpression != null)
		q = !sortExpression.EndsWith("DESC") ? q.OrderBy(p => p.Name) : q.OrderByDescending(p => p.Name);
	return q.Skip(startRowIndex ?? 0).Take(maximumRows ?? 10);
}
Paging requires the total count of all records to determine which buttons to show. With traditional ASP.NET there would have to be two separate methods, one for the data and another for the count. With the Navigation Framework all the information is held in StateContext Data so the one Search method can return the data as well as set the total count. So change the Search method to set the totalRowCount in StateContext Data before returning as shown below.

StateContext.Data["totalRowCount"] = q.Count();
return q.Skip(startRowIndex ?? 0).Take(maximumRows ?? 10);
Pressing F5 will show the Person List. Add a Pager control below the GridView as shown below.

<cc1:Pager ID="Pager1" runat="server">
	<Fields>
		<asp:NextPreviousPagerField ButtonType="Button" ShowFirstPageButton="true" ShowLastPageButton="true" />
	</Fields>
</cc1:Pager>
To enable the paging buttons add more data to the People in the PersonSearch class as shown below. This time pressing F5 will show fully functioning paging buttons. Selecting a Person and then the Hyperlink on the Details.aspx will return to the Listing.aspx with the page number retained, i.e., the Crumb Trail has remembered the page number.

private static List<Person> _People = new List<Person>()
{
	new Person(){Id = 1, Name = "Bob", DateOfBirth = new DateTime(1980, 12, 12)},
	new Person(){Id = 2, Name = "Brenda", DateOfBirth = new DateTime(1970, 6,1)},
	new Person(){Id = 3, Name = "Barney", DateOfBirth = new DateTime(1960, 10,25)},
	new Person(){Id = 4, Name = "Billy" , DateOfBirth = new DateTime(1980, 12,12)},
	new Person(){Id = 5, Name = "Bertha", DateOfBirth = new DateTime(1970, 6, 1)},
	new Person(){Id = 6, Name = "Bert", DateOfBirth = new DateTime(1960, 10, 25)},
	new Person(){Id = 7, Name = "Benny" , DateOfBirth = new DateTime(1980, 12,12)},
	new Person(){Id = 8, Name = "Bella", DateOfBirth = new DateTime(1970, 6, 1)},
	new Person(){Id = 9, Name = "Bridget", DateOfBirth = new DateTime(1960,10,25)},
	new Person(){Id = 10, Name = "Beth" , DateOfBirth = new DateTime(1980, 12,12)},
	new Person(){Id = 11, Name = "Brian", DateOfBirth = new DateTime(1970, 6, 1)},
	new Person(){Id = 12, Name = "Bessie", DateOfBirth = new DateTime(1960,10,25)},
};
Pressing the Search button should reset the page number, although currently it does not. As an example, Press F5, go to the second page then enter ‘bob’ into the Name Text Box and press Search. This should return one Person but actually returns none since it is searching for the second page of ‘bob’s. As was done for sorting, add a NavigationDataParameter to the UpdateParameters of the FormView’s NavigationDataSource as shown below, with its Reset property to true. This time when the Search button is pressed the page number is reset.

<cc1:NavigationDataSource ID="NavigationDataSource1" runat="server">
	<UpdateParameters>
		<cc1:NavigationDataParameter Name="sortExpression" Reset="true"/>
		<cc1:NavigationDataParameter Name="startRowIndex" Reset="true"/>
	</UpdateParameters>
</cc1:NavigationDataSource>
Currently, clicking the Pager control will cause a PostBack. However to change it to use refresh Navigation (Hyperlink) set its QueryStringField property as shown below (any non-empty string will do). Pressing F5 will show the Pager rendered as Hyperlinks. Filter and sort the list and click the Pager to see the search criteria and sort order are retained.

<cc1:Pager ID="Pager1" runat="server" QueryStringField="q">
	<Fields>
		<asp:NextPreviousPagerField ShowFirstPageButton="true" ShowLastPageButton="true" />
	</Fields>
</cc1:Pager>

Last edited Aug 4, 2013 at 7:27 PM by GrahamMendick, version 6